diff --git a/.cirrus.yml b/.cirrus.yml index ea1a978b65..cbc8a146ef 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -38,7 +38,7 @@ env: # Conda packages to be installed. CONDA_CACHE_PACKAGES: "nox pip" # Git commit hash for iris test data. - IRIS_TEST_DATA_VERSION: "2.8" + IRIS_TEST_DATA_VERSION: "2.9" # Base directory for the iris-test-data. IRIS_TEST_DATA_DIR: ${HOME}/iris-test-data diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 7ecce0c5fb..5d6e47a0da 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -64,6 +64,10 @@ This document explains the changes made to Iris for this release preserving the time of year. (:issue:`1422`, :issue:`4098`, :issue:`4665`, :pull:`4723`) +#. `@wjbenfold`_ and `@pp-mo`_ (reviewer) implemented the + :class:`~iris.coord_systems.PolarStereographic` CRS. (:issue:`4770`, + :pull:`4773`) + 🐛 Bugs Fixed ============= diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index 0bd7145a5f..802571925e 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -1061,32 +1061,39 @@ def __init__( false_northing=None, true_scale_lat=None, ellipsoid=None, + scale_factor_at_projection_origin=None, ): """ Constructs a Stereographic coord system. - Args: + Parameters + ---------- - * central_lat: + central_lat : float The latitude of the pole. - * central_lon: + central_lon : float The central longitude, which aligns with the y axis. - Kwargs: - - * false_easting: - X offset from planar origin in metres. Defaults to 0.0 . + false_easting : float, optional + X offset from planar origin in metres. - * false_northing: - Y offset from planar origin in metres. Defaults to 0.0 . + false_northing : float, optional + Y offset from planar origin in metres. - * true_scale_lat: + true_scale_lat : float, optional Latitude of true scale. - * ellipsoid (:class:`GeogCS`): + scale_factor_at_projection_origin : float, optional + Scale factor at the origin of the projection + + ellipsoid : :class:`GeogCS`, optional If given, defines the ellipsoid. + Notes + ----- + It is only valid to provide one of true_scale_lat and scale_factor_at_projection_origin + """ #: True latitude of planar origin in degrees. @@ -1105,27 +1112,42 @@ def __init__( self.true_scale_lat = _arg_default( true_scale_lat, None, cast_as=_float_or_None ) - # N.B. the way we use this parameter, we need it to default to None, + #: Scale factor at projection origin. + self.scale_factor_at_projection_origin = _arg_default( + scale_factor_at_projection_origin, None, cast_as=_float_or_None + ) + # N.B. the way we use these parameters, we need them to default to None, # and *not* to 0.0 . + if ( + self.true_scale_lat is not None + and self.scale_factor_at_projection_origin is not None + ): + raise ValueError( + "It does not make sense to provide both " + '"scale_factor_at_projection_origin" and "true_scale_latitude". ' + ) + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid - def __repr__(self): - return ( - "Stereographic(central_lat={!r}, central_lon={!r}, " - "false_easting={!r}, false_northing={!r}, " - "true_scale_lat={!r}, " - "ellipsoid={!r})".format( - self.central_lat, - self.central_lon, - self.false_easting, - self.false_northing, - self.true_scale_lat, - self.ellipsoid, + def _repr_attributes(self): + if self.scale_factor_at_projection_origin is None: + scale_info = "true_scale_lat={!r}, ".format(self.true_scale_lat) + else: + scale_info = "scale_factor_at_projection_origin={!r}, ".format( + self.scale_factor_at_projection_origin ) + return ( + f"(central_lat={self.central_lat}, central_lon={self.central_lon}, " + f"false_easting={self.false_easting}, false_northing={self.false_northing}, " + f"{scale_info}" + f"ellipsoid={self.ellipsoid})" ) + def __repr__(self): + return "Stereographic" + self._repr_attributes() + def as_cartopy_crs(self): globe = self._ellipsoid_to_globe(self.ellipsoid, ccrs.Globe()) @@ -1135,6 +1157,7 @@ def as_cartopy_crs(self): self.false_easting, self.false_northing, self.true_scale_lat, + self.scale_factor_at_projection_origin, globe=globe, ) @@ -1142,6 +1165,73 @@ def as_cartopy_projection(self): return self.as_cartopy_crs() +class PolarStereographic(Stereographic): + """ + A subclass of the stereographic map projection centred on a pole. + + """ + + grid_mapping_name = "polar_stereographic" + + def __init__( + self, + central_lat, + central_lon, + false_easting=None, + false_northing=None, + true_scale_lat=None, + scale_factor_at_projection_origin=None, + ellipsoid=None, + ): + """ + Construct a Polar Stereographic coord system. + + Parameters + ---------- + + central_lat : {90, -90} + The latitude of the pole. + + central_lon : float + The central longitude, which aligns with the y axis. + + false_easting : float, optional + X offset from planar origin in metres. + + false_northing : float, optional + Y offset from planar origin in metres. + + true_scale_lat : float, optional + Latitude of true scale. + + scale_factor_at_projection_origin : float, optional + Scale factor at the origin of the projection + + ellipsoid : :class:`GeogCS`, optional + If given, defines the ellipsoid. + + Notes + ----- + It is only valid to provide at most one of `true_scale_lat` and + `scale_factor_at_projection_origin`. + + + """ + + super().__init__( + central_lat=central_lat, + central_lon=central_lon, + false_easting=false_easting, + false_northing=false_northing, + true_scale_lat=true_scale_lat, + scale_factor_at_projection_origin=scale_factor_at_projection_origin, + ellipsoid=ellipsoid, + ) + + def __repr__(self): + return "PolarStereographic" + self._repr_attributes() + + class LambertConformal(CoordSystem): """ A coordinate system in the Lambert Conformal conic projection. diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index 4c5184deb1..09237d3f11 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -110,9 +110,13 @@ def action_default(engine): hh.build_transverse_mercator_coordinate_system, ), hh.CF_GRID_MAPPING_STEREO: ( - hh.has_supported_stereographic_parameters, + None, hh.build_stereographic_coordinate_system, ), + hh.CF_GRID_MAPPING_POLAR: ( + hh.has_supported_polar_stereographic_parameters, + hh.build_polar_stereographic_coordinate_system, + ), hh.CF_GRID_MAPPING_LAMBERT_CONFORMAL: ( None, hh.build_lambert_conformal_coordinate_system, diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 34eecdd310..e94fe99185 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -145,6 +145,7 @@ CF_ATTR_GRID_SEMI_MINOR_AXIS = "semi_minor_axis" CF_ATTR_GRID_LAT_OF_PROJ_ORIGIN = "latitude_of_projection_origin" CF_ATTR_GRID_LON_OF_PROJ_ORIGIN = "longitude_of_projection_origin" +CF_ATTR_GRID_STRAIGHT_VERT_LON = "straight_vertical_longitude_from_pole" CF_ATTR_GRID_STANDARD_PARALLEL = "standard_parallel" CF_ATTR_GRID_FALSE_EASTING = "false_easting" CF_ATTR_GRID_FALSE_NORTHING = "false_northing" @@ -418,8 +419,6 @@ def build_stereographic_coordinate_system(engine, cf_grid_var): ) false_easting = getattr(cf_grid_var, CF_ATTR_GRID_FALSE_EASTING, None) false_northing = getattr(cf_grid_var, CF_ATTR_GRID_FALSE_NORTHING, None) - # Iris currently only supports Stereographic projections with a scale - # factor of 1.0. This is checked elsewhere. cs = iris.coord_systems.Stereographic( latitude_of_projection_origin, @@ -433,6 +432,42 @@ def build_stereographic_coordinate_system(engine, cf_grid_var): return cs +################################################################################ +def build_polar_stereographic_coordinate_system(engine, cf_grid_var): + """ + Create a polar stereographic coordinate system from the CF-netCDF + grid mapping variable. + + """ + ellipsoid = _get_ellipsoid(cf_grid_var) + + latitude_of_projection_origin = getattr( + cf_grid_var, CF_ATTR_GRID_LAT_OF_PROJ_ORIGIN, None + ) + longitude_of_projection_origin = getattr( + cf_grid_var, CF_ATTR_GRID_STRAIGHT_VERT_LON, None + ) + true_scale_lat = getattr(cf_grid_var, CF_ATTR_GRID_STANDARD_PARALLEL, None) + scale_factor_at_projection_origin = getattr( + cf_grid_var, CF_ATTR_GRID_SCALE_FACTOR_AT_PROJ_ORIGIN, None + ) + + false_easting = getattr(cf_grid_var, CF_ATTR_GRID_FALSE_EASTING, None) + false_northing = getattr(cf_grid_var, CF_ATTR_GRID_FALSE_NORTHING, None) + + cs = iris.coord_systems.PolarStereographic( + latitude_of_projection_origin, + longitude_of_projection_origin, + false_easting, + false_northing, + true_scale_lat, + scale_factor_at_projection_origin, + ellipsoid=ellipsoid, + ) + + return cs + + ################################################################################ def build_mercator_coordinate_system(engine, cf_grid_var): """ @@ -1239,24 +1274,45 @@ def has_supported_mercator_parameters(engine, cf_name): ################################################################################ -def has_supported_stereographic_parameters(engine, cf_name): - """Determine whether the CF grid mapping variable has a value of 1.0 - for the scale_factor_at_projection_origin attribute.""" +def has_supported_polar_stereographic_parameters(engine, cf_name): + """Determine whether the CF grid mapping variable has the supported + values for the parameters of the Polar Stereographic projection.""" is_valid = True cf_grid_var = engine.cf_var.cf_group[cf_name] + latitude_of_projection_origin = getattr( + cf_grid_var, CF_ATTR_GRID_LAT_OF_PROJ_ORIGIN, None + ) + + standard_parallel = getattr( + cf_grid_var, CF_ATTR_GRID_STANDARD_PARALLEL, None + ) scale_factor_at_projection_origin = getattr( cf_grid_var, CF_ATTR_GRID_SCALE_FACTOR_AT_PROJ_ORIGIN, None ) + if ( + latitude_of_projection_origin != 90 + and latitude_of_projection_origin != -90 + ): + warnings.warn('"latitude_of_projection_origin" must be +90 or -90.') + is_valid = False + if ( scale_factor_at_projection_origin is not None - and scale_factor_at_projection_origin != 1 + and standard_parallel is not None ): warnings.warn( - "Scale factors other than 1.0 not yet supported for " - "stereographic projections" + "It does not make sense to provide both " + '"scale_factor_at_projection_origin" and "standard_parallel".' + ) + is_valid = False + + if scale_factor_at_projection_origin is None and standard_parallel is None: + warnings.warn( + 'One of "scale_factor_at_projection_origin" and ' + '"standard_parallel" is required.' ) is_valid = False diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 7a0e4e655d..6a7b37a1cc 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -2685,27 +2685,46 @@ def add_ellipsoid(ellipsoid): cf_var_grid.false_easting = cs.false_easting cf_var_grid.false_northing = cs.false_northing - # stereo - elif isinstance(cs, iris.coord_systems.Stereographic): + # polar stereo (have to do this before Stereographic because it subclasses it) + elif isinstance(cs, iris.coord_systems.PolarStereographic): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.latitude_of_projection_origin = cs.central_lat + cf_var_grid.straight_vertical_longitude_from_pole = ( + cs.central_lon + ) + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + # Only one of these should be set if cs.true_scale_lat is not None: - warnings.warn( - "Stereographic coordinate systems with " - "true scale latitude specified are not " - "yet handled" + cf_var_grid.true_scale_lat = cs.true_scale_lat + elif cs.scale_factor_at_projection_origin is not None: + cf_var_grid.scale_factor_at_projection_origin = ( + cs.scale_factor_at_projection_origin ) else: - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.longitude_of_projection_origin = ( - cs.central_lon + cf_var_grid.scale_factor_at_projection_origin = 1.0 + + # stereo + elif isinstance(cs, iris.coord_systems.Stereographic): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.longitude_of_projection_origin = cs.central_lon + cf_var_grid.latitude_of_projection_origin = cs.central_lat + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + # Only one of these should be set + if cs.true_scale_lat is not None: + msg = ( + "It is not valid CF to save a true_scale_lat for " + "a Stereographic grid mapping." ) - cf_var_grid.latitude_of_projection_origin = ( - cs.central_lat + raise ValueError(msg) + elif cs.scale_factor_at_projection_origin is not None: + cf_var_grid.scale_factor_at_projection_origin = ( + cs.scale_factor_at_projection_origin ) - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - # The Stereographic class has an implicit scale - # factor + else: cf_var_grid.scale_factor_at_projection_origin = 1.0 # osgb (a specific tmerc) diff --git a/lib/iris/tests/results/coord_systems/PolarStereographic.xml b/lib/iris/tests/results/coord_systems/PolarStereographic.xml new file mode 100644 index 0000000000..85abfc892f --- /dev/null +++ b/lib/iris/tests/results/coord_systems/PolarStereographic.xml @@ -0,0 +1,2 @@ + + diff --git a/lib/iris/tests/results/coord_systems/PolarStereographicScaleFactor.xml b/lib/iris/tests/results/coord_systems/PolarStereographicScaleFactor.xml new file mode 100644 index 0000000000..2fc1554cd7 --- /dev/null +++ b/lib/iris/tests/results/coord_systems/PolarStereographicScaleFactor.xml @@ -0,0 +1,2 @@ + + diff --git a/lib/iris/tests/results/coord_systems/PolarStereographicStandardParallel.xml b/lib/iris/tests/results/coord_systems/PolarStereographicStandardParallel.xml new file mode 100644 index 0000000000..de7b5f902c --- /dev/null +++ b/lib/iris/tests/results/coord_systems/PolarStereographicStandardParallel.xml @@ -0,0 +1,2 @@ + + diff --git a/lib/iris/tests/results/coord_systems/Stereographic.xml b/lib/iris/tests/results/coord_systems/Stereographic.xml index bb12cd94cc..fb338a8e4d 100644 --- a/lib/iris/tests/results/coord_systems/Stereographic.xml +++ b/lib/iris/tests/results/coord_systems/Stereographic.xml @@ -1,2 +1,2 @@ - + diff --git a/lib/iris/tests/results/netcdf/netcdf_polar.cml b/lib/iris/tests/results/netcdf/netcdf_polar.cml new file mode 100644 index 0000000000..ef76a61699 --- /dev/null +++ b/lib/iris/tests/results/netcdf/netcdf_polar.cml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/netcdf/netcdf_stereo.cml b/lib/iris/tests/results/netcdf/netcdf_stereo.cml index b07304cd62..c2d0bab03f 100644 --- a/lib/iris/tests/results/netcdf/netcdf_stereo.cml +++ b/lib/iris/tests/results/netcdf/netcdf_stereo.cml @@ -56,13 +56,13 @@ - + - + diff --git a/lib/iris/tests/test_coordsystem.py b/lib/iris/tests/test_coordsystem.py index 212c04bd7e..4497e77903 100644 --- a/lib/iris/tests/test_coordsystem.py +++ b/lib/iris/tests/test_coordsystem.py @@ -14,7 +14,6 @@ GeogCS, LambertConformal, RotatedGeogCS, - Stereographic, TransverseMercator, ) import iris.coords @@ -33,16 +32,6 @@ def osgb(): ) -def stereo(): - return Stereographic( - central_lat=-90, - central_lon=-45, - false_easting=100, - false_northing=200, - ellipsoid=GeogCS(6377563.396, 6356256.909), - ) - - class TestCoordSystemLookup(tests.IrisTest): def setUp(self): self.cube = iris.tests.stock.lat_lon_cube() @@ -519,85 +508,6 @@ def test_as_cartopy_projection(self): self.assertEqual(res, expected) -class Test_Stereographic_construction(tests.IrisTest): - def test_stereo(self): - st = stereo() - self.assertXMLElement(st, ("coord_systems", "Stereographic.xml")) - - -class Test_Stereographic_repr(tests.IrisTest): - def test_stereo(self): - st = stereo() - expected = ( - "Stereographic(central_lat=-90.0, central_lon=-45.0, " - "false_easting=100.0, false_northing=200.0, true_scale_lat=None, " - "ellipsoid=GeogCS(semi_major_axis=6377563.396, semi_minor_axis=6356256.909))" - ) - self.assertEqual(expected, repr(st)) - - -class Test_Stereographic_as_cartopy_crs(tests.IrisTest): - def test_as_cartopy_crs(self): - latitude_of_projection_origin = -90.0 - longitude_of_projection_origin = -45.0 - false_easting = 100.0 - false_northing = 200.0 - ellipsoid = GeogCS(6377563.396, 6356256.909) - - st = Stereographic( - central_lat=latitude_of_projection_origin, - central_lon=longitude_of_projection_origin, - false_easting=false_easting, - false_northing=false_northing, - ellipsoid=ellipsoid, - ) - expected = ccrs.Stereographic( - central_latitude=latitude_of_projection_origin, - central_longitude=longitude_of_projection_origin, - false_easting=false_easting, - false_northing=false_northing, - globe=ccrs.Globe( - semimajor_axis=6377563.396, - semiminor_axis=6356256.909, - ellipse=None, - ), - ) - - res = st.as_cartopy_crs() - self.assertEqual(res, expected) - - -class Test_Stereographic_as_cartopy_projection(tests.IrisTest): - def test_as_cartopy_projection(self): - latitude_of_projection_origin = -90.0 - longitude_of_projection_origin = -45.0 - false_easting = 100.0 - false_northing = 200.0 - ellipsoid = GeogCS(6377563.396, 6356256.909) - - st = Stereographic( - central_lat=latitude_of_projection_origin, - central_lon=longitude_of_projection_origin, - false_easting=false_easting, - false_northing=false_northing, - ellipsoid=ellipsoid, - ) - expected = ccrs.Stereographic( - central_latitude=latitude_of_projection_origin, - central_longitude=longitude_of_projection_origin, - false_easting=false_easting, - false_northing=false_northing, - globe=ccrs.Globe( - semimajor_axis=6377563.396, - semiminor_axis=6356256.909, - ellipse=None, - ), - ) - - res = st.as_cartopy_projection() - self.assertEqual(res, expected) - - class Test_LambertConformal(tests.GraphicsTest): def test_fail_secant_latitudes_none(self): emsg = "secant latitudes" diff --git a/lib/iris/tests/test_netcdf.py b/lib/iris/tests/test_netcdf.py index 4d130c0f0e..5029f822e7 100644 --- a/lib/iris/tests/test_netcdf.py +++ b/lib/iris/tests/test_netcdf.py @@ -248,6 +248,16 @@ def test_load_stereographic_grid(self): ) self.assertCML(cube, ("netcdf", "netcdf_stereo.cml")) + def test_load_polar_stereographic_grid(self): + # Test loading a single CF-netCDF file with a polar stereographic + # grid_mapping. + cube = iris.load_cube( + tests.get_data_path( + ("NetCDF", "polar", "toa_brightness_temperature.nc") + ) + ) + self.assertCML(cube, ("netcdf", "netcdf_polar.cml")) + def test_cell_methods(self): # Test exercising CF-netCDF cell method parsing. cubes = iris.load( diff --git a/lib/iris/tests/unit/coord_systems/test_PolarStereographic.py b/lib/iris/tests/unit/coord_systems/test_PolarStereographic.py new file mode 100755 index 0000000000..25f5d24800 --- /dev/null +++ b/lib/iris/tests/unit/coord_systems/test_PolarStereographic.py @@ -0,0 +1,251 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +"""Unit tests for the :class:`iris.coord_systems.PolarStereographic` class.""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests # isort:skip + +import cartopy.crs as ccrs + +from iris.coord_systems import GeogCS, PolarStereographic + + +class Test_PolarStereographic__basics(tests.IrisTest): + def setUp(self): + self.ps_blank = PolarStereographic( + central_lat=90.0, + central_lon=0, + ellipsoid=GeogCS(6377563.396, 6356256.909), + ) + self.ps_standard_parallel = PolarStereographic( + central_lat=90.0, + central_lon=0, + true_scale_lat=30, + ellipsoid=GeogCS(6377563.396, 6356256.909), + ) + self.ps_scale_factor = PolarStereographic( + central_lat=90.0, + central_lon=0, + scale_factor_at_projection_origin=1.1, + ellipsoid=GeogCS(6377563.396, 6356256.909), + ) + + def test_construction(self): + self.assertXMLElement( + self.ps_blank, ("coord_systems", "PolarStereographic.xml") + ) + + def test_construction_sp(self): + self.assertXMLElement( + self.ps_standard_parallel, + ("coord_systems", "PolarStereographicStandardParallel.xml"), + ) + + def test_construction_sf(self): + self.assertXMLElement( + self.ps_scale_factor, + ("coord_systems", "PolarStereographicScaleFactor.xml"), + ) + + def test_repr_blank(self): + expected = ( + "PolarStereographic(central_lat=90.0, central_lon=0.0, " + "false_easting=0.0, false_northing=0.0, " + "true_scale_lat=None, " + "ellipsoid=GeogCS(semi_major_axis=6377563.396, " + "semi_minor_axis=6356256.909))" + ) + self.assertEqual(expected, repr(self.ps_blank)) + + def test_repr_standard_parallel(self): + expected = ( + "PolarStereographic(central_lat=90.0, central_lon=0.0, " + "false_easting=0.0, false_northing=0.0, " + "true_scale_lat=30.0, " + "ellipsoid=GeogCS(semi_major_axis=6377563.396, " + "semi_minor_axis=6356256.909))" + ) + self.assertEqual(expected, repr(self.ps_standard_parallel)) + + def test_repr_scale_factor(self): + expected = ( + "PolarStereographic(central_lat=90.0, central_lon=0.0, " + "false_easting=0.0, false_northing=0.0, " + "scale_factor_at_projection_origin=1.1, " + "ellipsoid=GeogCS(semi_major_axis=6377563.396, " + "semi_minor_axis=6356256.909))" + ) + self.assertEqual(expected, repr(self.ps_scale_factor)) + + +class Test_init_defaults(tests.IrisTest): + def test_set_optional_args(self): + # Check that setting the optional (non-ellipse) args works. + crs = PolarStereographic( + central_lat=90, + central_lon=50, + false_easting=13, + false_northing=12, + true_scale_lat=32, + ) + self.assertEqualAndKind(crs.central_lat, 90.0) + self.assertEqualAndKind(crs.central_lon, 50.0) + self.assertEqualAndKind(crs.false_easting, 13.0) + self.assertEqualAndKind(crs.false_northing, 12.0) + self.assertEqualAndKind(crs.true_scale_lat, 32.0) + + def test_set_optional_scale_factor_alternative(self): + # Check that setting the optional (non-ellipse) args works. + crs = PolarStereographic( + central_lat=-90, + central_lon=50, + false_easting=13, + false_northing=12, + scale_factor_at_projection_origin=3.1, + ) + self.assertEqualAndKind(crs.central_lat, -90.0) + self.assertEqualAndKind(crs.central_lon, 50.0) + self.assertEqualAndKind(crs.false_easting, 13.0) + self.assertEqualAndKind(crs.false_northing, 12.0) + self.assertEqualAndKind(crs.scale_factor_at_projection_origin, 3.1) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.false_easting, 0.0) + self.assertEqualAndKind(crs.false_northing, 0.0) + self.assertEqualAndKind(crs.true_scale_lat, None) + self.assertEqualAndKind(crs.scale_factor_at_projection_origin, None) + + def test_no_optional_args(self): + # Check expected defaults with no optional args. + crs = PolarStereographic( + central_lat=-90, + central_lon=50, + ) + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected defaults with optional args=None. + crs = PolarStereographic( + central_lat=-90, + central_lon=50, + true_scale_lat=None, + scale_factor_at_projection_origin=None, + false_easting=None, + false_northing=None, + ) + self._check_crs_defaults(crs) + + +class AsCartopyMixin: + def test_simple(self): + # Check that a projection set up with all the defaults is correctly + # converted to a cartopy CRS. + central_lat = -90 + central_lon = 50 + polar_cs = PolarStereographic( + central_lat=central_lat, + central_lon=central_lon, + ) + res = self.as_cartopy_method(polar_cs) + expected = ccrs.Stereographic( + central_latitude=central_lat, + central_longitude=central_lon, + globe=ccrs.Globe(), + ) + self.assertEqual(res, expected) + + def test_extra_kwargs_scale_factor(self): + # Check that a projection with non-default values is correctly + # converted to a cartopy CRS. + central_lat = -90 + central_lon = 50 + scale_factor_at_projection_origin = 1.3 + false_easting = 13 + false_northing = 15 + ellipsoid = GeogCS( + semi_major_axis=6377563.396, semi_minor_axis=6356256.909 + ) + + polar_cs = PolarStereographic( + central_lat=central_lat, + central_lon=central_lon, + scale_factor_at_projection_origin=scale_factor_at_projection_origin, + false_easting=false_easting, + false_northing=false_northing, + ellipsoid=ellipsoid, + ) + + expected = ccrs.Stereographic( + central_latitude=central_lat, + central_longitude=central_lon, + false_easting=false_easting, + false_northing=false_northing, + scale_factor=scale_factor_at_projection_origin, + globe=ccrs.Globe( + semimajor_axis=6377563.396, + semiminor_axis=6356256.909, + ellipse=None, + ), + ) + + res = self.as_cartopy_method(polar_cs) + self.assertEqual(res, expected) + + def test_extra_kwargs_true_scale_lat_alternative(self): + # Check that a projection with non-default values is correctly + # converted to a cartopy CRS. + central_lat = -90 + central_lon = 50 + true_scale_lat = 80 + false_easting = 13 + false_northing = 15 + ellipsoid = GeogCS( + semi_major_axis=6377563.396, semi_minor_axis=6356256.909 + ) + + polar_cs = PolarStereographic( + central_lat=central_lat, + central_lon=central_lon, + true_scale_lat=true_scale_lat, + false_easting=false_easting, + false_northing=false_northing, + ellipsoid=ellipsoid, + ) + + expected = ccrs.Stereographic( + central_latitude=central_lat, + central_longitude=central_lon, + false_easting=false_easting, + false_northing=false_northing, + true_scale_latitude=true_scale_lat, + globe=ccrs.Globe( + semimajor_axis=6377563.396, + semiminor_axis=6356256.909, + ellipse=None, + ), + ) + + res = self.as_cartopy_method(polar_cs) + self.assertEqual(res, expected) + + +class Test_PolarStereographic__as_cartopy_crs(tests.IrisTest, AsCartopyMixin): + def setUp(self): + self.as_cartopy_method = PolarStereographic.as_cartopy_crs + + +class Test_PolarStereographic__as_cartopy_projection( + tests.IrisTest, AsCartopyMixin +): + def setUp(self): + self.as_cartopy_method = PolarStereographic.as_cartopy_projection + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/coord_systems/test_Stereographic.py b/lib/iris/tests/unit/coord_systems/test_Stereographic.py index fac411f9d5..acd77112c1 100644 --- a/lib/iris/tests/unit/coord_systems/test_Stereographic.py +++ b/lib/iris/tests/unit/coord_systems/test_Stereographic.py @@ -9,12 +9,29 @@ # importing anything else. import iris.tests as tests # isort:skip -from iris.coord_systems import Stereographic +import cartopy.crs as ccrs + +from iris.coord_systems import GeogCS, Stereographic + + +def stereo(**kwargs): + return Stereographic( + central_lat=-90, + central_lon=-45, + false_easting=100, + false_northing=200, + ellipsoid=GeogCS(6377563.396, 6356256.909), + **kwargs, + ) + + +class Test_Stereographic_construction(tests.IrisTest): + def test_stereo(self): + st = stereo() + self.assertXMLElement(st, ("coord_systems", "Stereographic.xml")) class Test_init_defaults(tests.IrisTest): - # NOTE: most of the testing for Stereographic is in the legacy test module - # 'iris.tests.test_coordsystem'. # This class *only* tests the defaults for optional constructor args. def test_set_optional_args(self): @@ -26,12 +43,26 @@ def test_set_optional_args(self): self.assertEqualAndKind(crs.false_northing, -203.7) self.assertEqualAndKind(crs.true_scale_lat, 77.0) + def test_set_optional_args_scale_factor_alternative(self): + # Check that setting the optional (non-ellipse) args works. + crs = Stereographic( + 0, + 0, + false_easting=100, + false_northing=-203.7, + scale_factor_at_projection_origin=1.3, + ) + self.assertEqualAndKind(crs.false_easting, 100.0) + self.assertEqualAndKind(crs.false_northing, -203.7) + self.assertEqualAndKind(crs.scale_factor_at_projection_origin, 1.3) + def _check_crs_defaults(self, crs): # Check for property defaults when no kwargs options were set. # NOTE: except ellipsoid, which is done elsewhere. self.assertEqualAndKind(crs.false_easting, 0.0) self.assertEqualAndKind(crs.false_northing, 0.0) self.assertIsNone(crs.true_scale_lat) + self.assertIsNone(crs.scale_factor_at_projection_origin) def test_no_optional_args(self): # Check expected defaults with no optional args. @@ -41,10 +72,141 @@ def test_no_optional_args(self): def test_optional_args_None(self): # Check expected defaults with optional args=None. crs = Stereographic( - 0, 0, false_easting=None, false_northing=None, true_scale_lat=None + 0, + 0, + false_easting=None, + false_northing=None, + true_scale_lat=None, + scale_factor_at_projection_origin=None, ) self._check_crs_defaults(crs) +class Test_Stereographic_repr(tests.IrisTest): + def test_stereo(self): + st = stereo() + expected = ( + "Stereographic(central_lat=-90.0, central_lon=-45.0, " + "false_easting=100.0, false_northing=200.0, true_scale_lat=None, " + "ellipsoid=GeogCS(semi_major_axis=6377563.396, semi_minor_axis=6356256.909))" + ) + self.assertEqual(expected, repr(st)) + + def test_stereo_scale_factor(self): + st = stereo(scale_factor_at_projection_origin=0.9) + expected = ( + "Stereographic(central_lat=-90.0, central_lon=-45.0, " + "false_easting=100.0, false_northing=200.0, " + "scale_factor_at_projection_origin=0.9, " + "ellipsoid=GeogCS(semi_major_axis=6377563.396, semi_minor_axis=6356256.909))" + ) + self.assertEqual(expected, repr(st)) + + +class AsCartopyMixin: + def test_basic(self): + latitude_of_projection_origin = -90.0 + longitude_of_projection_origin = -45.0 + false_easting = 100.0 + false_northing = 200.0 + ellipsoid = GeogCS(6377563.396, 6356256.909) + + st = Stereographic( + central_lat=latitude_of_projection_origin, + central_lon=longitude_of_projection_origin, + false_easting=false_easting, + false_northing=false_northing, + ellipsoid=ellipsoid, + ) + expected = ccrs.Stereographic( + central_latitude=latitude_of_projection_origin, + central_longitude=longitude_of_projection_origin, + false_easting=false_easting, + false_northing=false_northing, + globe=ccrs.Globe( + semimajor_axis=6377563.396, + semiminor_axis=6356256.909, + ellipse=None, + ), + ) + + res = self.as_cartopy_method(st) + self.assertEqual(res, expected) + + def test_true_scale_lat(self): + latitude_of_projection_origin = -90.0 + longitude_of_projection_origin = -45.0 + false_easting = 100.0 + false_northing = 200.0 + true_scale_lat = 30 + ellipsoid = GeogCS(6377563.396, 6356256.909) + + st = Stereographic( + central_lat=latitude_of_projection_origin, + central_lon=longitude_of_projection_origin, + false_easting=false_easting, + false_northing=false_northing, + true_scale_lat=true_scale_lat, + ellipsoid=ellipsoid, + ) + expected = ccrs.Stereographic( + central_latitude=latitude_of_projection_origin, + central_longitude=longitude_of_projection_origin, + false_easting=false_easting, + false_northing=false_northing, + true_scale_latitude=true_scale_lat, + globe=ccrs.Globe( + semimajor_axis=6377563.396, + semiminor_axis=6356256.909, + ellipse=None, + ), + ) + + res = self.as_cartopy_method(st) + self.assertEqual(res, expected) + + def test_scale_factor(self): + latitude_of_projection_origin = -90.0 + longitude_of_projection_origin = -45.0 + false_easting = 100.0 + false_northing = 200.0 + scale_factor_at_projection_origin = 0.9 + ellipsoid = GeogCS(6377563.396, 6356256.909) + + st = Stereographic( + central_lat=latitude_of_projection_origin, + central_lon=longitude_of_projection_origin, + false_easting=false_easting, + false_northing=false_northing, + scale_factor_at_projection_origin=scale_factor_at_projection_origin, + ellipsoid=ellipsoid, + ) + expected = ccrs.Stereographic( + central_latitude=latitude_of_projection_origin, + central_longitude=longitude_of_projection_origin, + false_easting=false_easting, + false_northing=false_northing, + scale_factor=scale_factor_at_projection_origin, + globe=ccrs.Globe( + semimajor_axis=6377563.396, + semiminor_axis=6356256.909, + ellipse=None, + ), + ) + + res = self.as_cartopy_method(st) + self.assertEqual(res, expected) + + +class Test_Stereographic_as_cartopy_crs(tests.IrisTest, AsCartopyMixin): + def setUp(self): + self.as_cartopy_method = Stereographic.as_cartopy_crs + + +class Test_Stereographic_as_cartopy_projection(tests.IrisTest, AsCartopyMixin): + def setUp(self): + self.as_cartopy_method = Stereographic.as_cartopy_projection + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py index 1bf9226092..a56ef5e754 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py @@ -144,7 +144,6 @@ def _make_testcase_cdl( # Add a specified scale-factor, if requested. if mapping_scalefactor is not None: # Add a specific scale-factor term to the grid mapping. - # (Non-unity scale is not supported for Mercator/Stereographic). sfapo_name = hh.CF_ATTR_GRID_SCALE_FACTOR_AT_PROJ_ORIGIN g_string += f""" {g_varname}:{sfapo_name} = {mapping_scalefactor} ; @@ -197,6 +196,22 @@ def _make_testcase_cdl( g_string += f""" {g_varname}:{saa_name} = "y" ; """ + # Polar stereo needs a special 'latitude of projection origin', a + # 'straight_vertical_longitude_from_pole' and a `standard_parallel` or + # `scale_factor_at_projection_origin` so treat it specially + if mapping_type_name in (hh.CF_GRID_MAPPING_POLAR,): + latpo_name = hh.CF_ATTR_GRID_LAT_OF_PROJ_ORIGIN + g_string += f""" + {g_varname}:{latpo_name} = 90.0 ; + """ + svl_name = hh.CF_ATTR_GRID_STRAIGHT_VERT_LON + g_string += f""" + {g_varname}:{svl_name} = 0.0 ; + """ + stanpar_name = hh.CF_ATTR_GRID_STANDARD_PARALLEL + g_string += f""" + {g_varname}:{stanpar_name} = 1.0 ; + """ # y-coord values if yco_values is None: @@ -445,8 +460,7 @@ def test_mapping_rotated(self): # # All non-latlon coordinate systems ... # These all have projection-x/y coordinates with units of metres. - # They all work the same way, except that Stereographic has - # parameter checking routines that can fail. + # They all work the same way. # NOTE: various mapping types *require* certain addtional properties # - without which an error will occur during translation. # - run_testcase/_make_testcase_cdl know how to provide these @@ -494,28 +508,9 @@ def test_mapping_stereographic(self): result = self.run_testcase(mapping_type_name=hh.CF_GRID_MAPPING_STEREO) self.check_result(result, cube_cstype=ics.Stereographic) - def test_mapping_stereographic__fail_unsupported(self): - # Provide a non-unity scale factor, which we cannot handle. - # Result : fails to convert into a coord-system, and emits a warning. - # - # Rules Triggered: - # 001 : fc_default - # 002 : fc_provides_grid_mapping_(stereographic) --(FAILED check has_supported_stereographic_parameters) - # 003 : fc_provides_coordinate_(projection_y) - # 004 : fc_provides_coordinate_(projection_x) - # 005 : fc_build_coordinate_(projection_y)(FAILED projected coord with non-projected cs) - # 006 : fc_build_coordinate_(projection_x)(FAILED projected coord with non-projected cs) - # Notes: - # * grid-mapping identified : NONE - # * dim-coords identified : proj-x and -y - # * coords built : NONE (no dim or aux coords: cube has no coords) - warning = "not yet supported for stereographic" - result = self.run_testcase( - warning=warning, - mapping_type_name=hh.CF_GRID_MAPPING_STEREO, - mapping_scalefactor=2.0, - ) - self.check_result(result, cube_no_cs=True, cube_no_xycoords=True) + def test_mapping_polar_stereographic(self): + result = self.run_testcase(mapping_type_name=hh.CF_GRID_MAPPING_POLAR) + self.check_result(result, cube_cstype=ics.PolarStereographic) def test_mapping_transverse_mercator(self): result = self.run_testcase( diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_polar_stereographic_coordinate_system.py b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_polar_stereographic_coordinate_system.py new file mode 100755 index 0000000000..09cfde9d5b --- /dev/null +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_polar_stereographic_coordinate_system.py @@ -0,0 +1,150 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Test function :func:`iris.fileformats._nc_load_rules.helpers.\ +build_polar_stereographic_coordinate_system`. + +""" + +# import iris tests first so that some things can be initialised before +# importing anything else +import iris.tests as tests # isort:skip + +from unittest import mock + +import iris +from iris.coord_systems import PolarStereographic +from iris.fileformats._nc_load_rules.helpers import ( + build_polar_stereographic_coordinate_system, +) + + +class TestBuildPolarStereographicCoordinateSystem(tests.IrisTest): + def test_valid_north(self): + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=90, + scale_factor_at_projection_origin=1, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + + cs = build_polar_stereographic_coordinate_system(None, cf_grid_var) + + expected = PolarStereographic( + central_lon=(cf_grid_var.straight_vertical_longitude_from_pole), + central_lat=(cf_grid_var.latitude_of_projection_origin), + scale_factor_at_projection_origin=( + cf_grid_var.scale_factor_at_projection_origin + ), + ellipsoid=iris.coord_systems.GeogCS( + cf_grid_var.semi_major_axis, cf_grid_var.semi_minor_axis + ), + ) + self.assertEqual(cs, expected) + + def test_valid_south(self): + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=-90, + scale_factor_at_projection_origin=1, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + + cs = build_polar_stereographic_coordinate_system(None, cf_grid_var) + + expected = PolarStereographic( + central_lon=(cf_grid_var.straight_vertical_longitude_from_pole), + central_lat=(cf_grid_var.latitude_of_projection_origin), + scale_factor_at_projection_origin=( + cf_grid_var.scale_factor_at_projection_origin + ), + ellipsoid=iris.coord_systems.GeogCS( + cf_grid_var.semi_major_axis, cf_grid_var.semi_minor_axis + ), + ) + self.assertEqual(cs, expected) + + def test_valid_with_standard_parallel(self): + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=90, + standard_parallel=30, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + + cs = build_polar_stereographic_coordinate_system(None, cf_grid_var) + + expected = PolarStereographic( + central_lon=(cf_grid_var.straight_vertical_longitude_from_pole), + central_lat=(cf_grid_var.latitude_of_projection_origin), + true_scale_lat=(cf_grid_var.standard_parallel), + ellipsoid=iris.coord_systems.GeogCS( + cf_grid_var.semi_major_axis, cf_grid_var.semi_minor_axis + ), + ) + self.assertEqual(cs, expected) + + def test_valid_with_false_easting_northing(self): + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=90, + scale_factor_at_projection_origin=1, + false_easting=30, + false_northing=40, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + + cs = build_polar_stereographic_coordinate_system(None, cf_grid_var) + + expected = PolarStereographic( + central_lon=(cf_grid_var.straight_vertical_longitude_from_pole), + central_lat=(cf_grid_var.latitude_of_projection_origin), + scale_factor_at_projection_origin=( + cf_grid_var.scale_factor_at_projection_origin + ), + false_easting=(cf_grid_var.false_easting), + false_northing=(cf_grid_var.false_northing), + ellipsoid=iris.coord_systems.GeogCS( + cf_grid_var.semi_major_axis, cf_grid_var.semi_minor_axis + ), + ) + self.assertEqual(cs, expected) + + def test_valid_nonzero_veritcal_lon(self): + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=30, + latitude_of_projection_origin=90, + scale_factor_at_projection_origin=1, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + + cs = build_polar_stereographic_coordinate_system(None, cf_grid_var) + + expected = PolarStereographic( + central_lon=(cf_grid_var.straight_vertical_longitude_from_pole), + central_lat=(cf_grid_var.latitude_of_projection_origin), + scale_factor_at_projection_origin=( + cf_grid_var.scale_factor_at_projection_origin + ), + ellipsoid=iris.coord_systems.GeogCS( + cf_grid_var.semi_major_axis, cf_grid_var.semi_minor_axis + ), + ) + self.assertEqual(cs, expected) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_has_supported_polar_stereographic_parameters.py b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_has_supported_polar_stereographic_parameters.py new file mode 100755 index 0000000000..6e6d6e4e81 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_has_supported_polar_stereographic_parameters.py @@ -0,0 +1,242 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Test function :func:`iris.fileformats._nc_load_rules.helpers.\ +has_supported_polar_stereographic_parameters`. + +""" + +from unittest import mock +import warnings + +from iris.fileformats._nc_load_rules.helpers import ( + has_supported_polar_stereographic_parameters, +) + +# import iris tests first so that some things can be initialised before +# importing anything else +import iris.tests as tests # isort:skip + + +def _engine(cf_grid_var, cf_name): + cf_group = {cf_name: cf_grid_var} + cf_var = mock.Mock(cf_group=cf_group) + return mock.Mock(cf_var=cf_var) + + +class TestHasSupportedPolarStereographicParameters(tests.IrisTest): + def test_valid_base_north(self): + cf_name = "polar_stereographic" + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=90, + false_easting=0, + false_northing=0, + scale_factor_at_projection_origin=1, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + engine = _engine(cf_grid_var, cf_name) + + is_valid = has_supported_polar_stereographic_parameters( + engine, cf_name + ) + + self.assertTrue(is_valid) + + def test_valid_base_south(self): + cf_name = "polar_stereographic" + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=-90, + false_easting=0, + false_northing=0, + scale_factor_at_projection_origin=1, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + engine = _engine(cf_grid_var, cf_name) + + is_valid = has_supported_polar_stereographic_parameters( + engine, cf_name + ) + + self.assertTrue(is_valid) + + def test_valid_straight_vertical_longitude(self): + cf_name = "polar_stereographic" + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=30, + latitude_of_projection_origin=90, + false_easting=0, + false_northing=0, + scale_factor_at_projection_origin=1, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + engine = _engine(cf_grid_var, cf_name) + + is_valid = has_supported_polar_stereographic_parameters( + engine, cf_name + ) + + self.assertTrue(is_valid) + + def test_valid_false_easting_northing(self): + cf_name = "polar_stereographic" + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=90, + false_easting=15, + false_northing=10, + scale_factor_at_projection_origin=1, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + engine = _engine(cf_grid_var, cf_name) + + is_valid = has_supported_polar_stereographic_parameters( + engine, cf_name + ) + + self.assertTrue(is_valid) + + def test_valid_standard_parallel(self): + cf_name = "polar_stereographic" + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=90, + false_easting=0, + false_northing=0, + standard_parallel=15, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + engine = _engine(cf_grid_var, cf_name) + + is_valid = has_supported_polar_stereographic_parameters( + engine, cf_name + ) + + self.assertTrue(is_valid) + + def test_valid_scale_factor(self): + cf_name = "polar_stereographic" + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=90, + false_easting=0, + false_northing=0, + scale_factor_at_projection_origin=0.9, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + engine = _engine(cf_grid_var, cf_name) + + is_valid = has_supported_polar_stereographic_parameters( + engine, cf_name + ) + + self.assertTrue(is_valid) + + def test_invalid_scale_factor_and_standard_parallel(self): + # Scale factor and standard parallel cannot both be specified for + # Polar Stereographic projections + cf_name = "polar_stereographic" + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=90, + false_easting=0, + false_northing=0, + scale_factor_at_projection_origin=0.9, + standard_parallel=20, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + engine = _engine(cf_grid_var, cf_name) + + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter("always") + is_valid = has_supported_polar_stereographic_parameters( + engine, cf_name + ) + + self.assertFalse(is_valid) + self.assertEqual(len(warns), 1) + self.assertRegex( + str(warns[0]), + "both " + '"scale_factor_at_projection_origin" and "standard_parallel"', + ) + + def test_absent_scale_factor_and_standard_parallel(self): + # Scale factor and standard parallel cannot both be specified for + # Polar Stereographic projections + cf_name = "polar_stereographic" + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=90, + false_easting=0, + false_northing=0, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + engine = _engine(cf_grid_var, cf_name) + + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter("always") + is_valid = has_supported_polar_stereographic_parameters( + engine, cf_name + ) + + self.assertFalse(is_valid) + self.assertEqual(len(warns), 1) + self.assertRegex( + str(warns[0]), + 'One of "scale_factor_at_projection_origin" and ' + '"standard_parallel" is required.', + ) + + def test_invalid_latitude_of_projection_origin(self): + # Scale factor and standard parallel cannot both be specified for + # Polar Stereographic projections + cf_name = "polar_stereographic" + cf_grid_var = mock.Mock( + spec=[], + straight_vertical_longitude_from_pole=0, + latitude_of_projection_origin=45, + false_easting=0, + false_northing=0, + scale_factor_at_projection_origin=1, + semi_major_axis=6377563.396, + semi_minor_axis=6356256.909, + ) + engine = _engine(cf_grid_var, cf_name) + + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter("always") + is_valid = has_supported_polar_stereographic_parameters( + engine, cf_name + ) + + self.assertFalse(is_valid) + self.assertEqual(len(warns), 1) + self.assertRegex( + str(warns[0]), + r'"latitude_of_projection_origin" must be \+90 or -90\.', + ) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_has_supported_stereographic_parameters.py b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_has_supported_stereographic_parameters.py deleted file mode 100644 index 8bec823f4b..0000000000 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_has_supported_stereographic_parameters.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright Iris contributors -# -# This file is part of Iris and is released under the LGPL license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -""" -Test function :func:`iris.fileformats._nc_load_rules.helpers.\ -has_supported_stereographic_parameters`. - -""" - -from unittest import mock -import warnings - -from iris.fileformats._nc_load_rules.helpers import ( - has_supported_stereographic_parameters, -) - -# import iris tests first so that some things can be initialised before -# importing anything else -import iris.tests as tests # isort:skip - - -def _engine(cf_grid_var, cf_name): - cf_group = {cf_name: cf_grid_var} - cf_var = mock.Mock(cf_group=cf_group) - return mock.Mock(cf_var=cf_var) - - -class TestHasSupportedStereographicParameters(tests.IrisTest): - def test_valid(self): - cf_name = "stereographic" - cf_grid_var = mock.Mock( - spec=[], - latitude_of_projection_origin=0, - longitude_of_projection_origin=0, - false_easting=-100, - false_northing=200, - scale_factor_at_projection_origin=1, - semi_major_axis=6377563.396, - semi_minor_axis=6356256.909, - ) - engine = _engine(cf_grid_var, cf_name) - - is_valid = has_supported_stereographic_parameters(engine, cf_name) - - self.assertTrue(is_valid) - - def test_invalid_scale_factor(self): - # Iris does not yet support scale factors other than one for - # stereographic projections - cf_name = "stereographic" - cf_grid_var = mock.Mock( - spec=[], - latitude_of_projection_origin=0, - longitude_of_projection_origin=0, - false_easting=-100, - false_northing=200, - scale_factor_at_projection_origin=0.9, - semi_major_axis=6377563.396, - semi_minor_axis=6356256.909, - ) - engine = _engine(cf_grid_var, cf_name) - - with warnings.catch_warnings(record=True) as warns: - warnings.simplefilter("always") - is_valid = has_supported_stereographic_parameters(engine, cf_name) - - self.assertFalse(is_valid) - self.assertEqual(len(warns), 1) - self.assertRegex(str(warns[0]), "Scale factor") - - -if __name__ == "__main__": - tests.main()