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