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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=============
Expand Down
138 changes: 114 additions & 24 deletions lib/iris/coord_systems.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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())

Expand All @@ -1135,13 +1157,81 @@ def as_cartopy_crs(self):
self.false_easting,
self.false_northing,
self.true_scale_lat,
self.scale_factor_at_projection_origin,
globe=globe,
)

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.
Expand Down
6 changes: 5 additions & 1 deletion lib/iris/fileformats/_nc_load_rules/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
72 changes: 64 additions & 8 deletions lib/iris/fileformats/_nc_load_rules/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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

Expand Down
51 changes: 35 additions & 16 deletions lib/iris/fileformats/netcdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading