Skip to content

Commit 149e2d1

Browse files
wjbenfoldpp-mo
andauthored
PolarStereographic implementation (#4773)
* PolarStereographic implementation * Test data version * What's new * Accept suggestion to lib/iris/coord_systems.py Co-authored-by: Patrick Peglar <[email protected]> * Permit Stereographic saving * Update what's new * Review fixes Co-authored-by: Patrick Peglar <[email protected]>
1 parent f4fb6fa commit 149e2d1

20 files changed

+1116
-247
lines changed

.cirrus.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ env:
3838
# Conda packages to be installed.
3939
CONDA_CACHE_PACKAGES: "nox pip"
4040
# Git commit hash for iris test data.
41-
IRIS_TEST_DATA_VERSION: "2.8"
41+
IRIS_TEST_DATA_VERSION: "2.9"
4242
# Base directory for the iris-test-data.
4343
IRIS_TEST_DATA_DIR: ${HOME}/iris-test-data
4444

docs/src/whatsnew/latest.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ This document explains the changes made to Iris for this release
6464
preserving the time of year. (:issue:`1422`, :issue:`4098`, :issue:`4665`,
6565
:pull:`4723`)
6666

67+
#. `@wjbenfold`_ and `@pp-mo`_ (reviewer) implemented the
68+
:class:`~iris.coord_systems.PolarStereographic` CRS. (:issue:`4770`,
69+
:pull:`4773`)
70+
6771

6872
🐛 Bugs Fixed
6973
=============

lib/iris/coord_systems.py

Lines changed: 114 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,32 +1061,39 @@ def __init__(
10611061
false_northing=None,
10621062
true_scale_lat=None,
10631063
ellipsoid=None,
1064+
scale_factor_at_projection_origin=None,
10641065
):
10651066
"""
10661067
Constructs a Stereographic coord system.
10671068
1068-
Args:
1069+
Parameters
1070+
----------
10691071
1070-
* central_lat:
1072+
central_lat : float
10711073
The latitude of the pole.
10721074
1073-
* central_lon:
1075+
central_lon : float
10741076
The central longitude, which aligns with the y axis.
10751077
1076-
Kwargs:
1077-
1078-
* false_easting:
1079-
X offset from planar origin in metres. Defaults to 0.0 .
1078+
false_easting : float, optional
1079+
X offset from planar origin in metres.
10801080
1081-
* false_northing:
1082-
Y offset from planar origin in metres. Defaults to 0.0 .
1081+
false_northing : float, optional
1082+
Y offset from planar origin in metres.
10831083
1084-
* true_scale_lat:
1084+
true_scale_lat : float, optional
10851085
Latitude of true scale.
10861086
1087-
* ellipsoid (:class:`GeogCS`):
1087+
scale_factor_at_projection_origin : float, optional
1088+
Scale factor at the origin of the projection
1089+
1090+
ellipsoid : :class:`GeogCS`, optional
10881091
If given, defines the ellipsoid.
10891092
1093+
Notes
1094+
-----
1095+
It is only valid to provide one of true_scale_lat and scale_factor_at_projection_origin
1096+
10901097
"""
10911098

10921099
#: True latitude of planar origin in degrees.
@@ -1105,27 +1112,42 @@ def __init__(
11051112
self.true_scale_lat = _arg_default(
11061113
true_scale_lat, None, cast_as=_float_or_None
11071114
)
1108-
# N.B. the way we use this parameter, we need it to default to None,
1115+
#: Scale factor at projection origin.
1116+
self.scale_factor_at_projection_origin = _arg_default(
1117+
scale_factor_at_projection_origin, None, cast_as=_float_or_None
1118+
)
1119+
# N.B. the way we use these parameters, we need them to default to None,
11091120
# and *not* to 0.0 .
11101121

1122+
if (
1123+
self.true_scale_lat is not None
1124+
and self.scale_factor_at_projection_origin is not None
1125+
):
1126+
raise ValueError(
1127+
"It does not make sense to provide both "
1128+
'"scale_factor_at_projection_origin" and "true_scale_latitude". '
1129+
)
1130+
11111131
#: Ellipsoid definition (:class:`GeogCS` or None).
11121132
self.ellipsoid = ellipsoid
11131133

1114-
def __repr__(self):
1115-
return (
1116-
"Stereographic(central_lat={!r}, central_lon={!r}, "
1117-
"false_easting={!r}, false_northing={!r}, "
1118-
"true_scale_lat={!r}, "
1119-
"ellipsoid={!r})".format(
1120-
self.central_lat,
1121-
self.central_lon,
1122-
self.false_easting,
1123-
self.false_northing,
1124-
self.true_scale_lat,
1125-
self.ellipsoid,
1134+
def _repr_attributes(self):
1135+
if self.scale_factor_at_projection_origin is None:
1136+
scale_info = "true_scale_lat={!r}, ".format(self.true_scale_lat)
1137+
else:
1138+
scale_info = "scale_factor_at_projection_origin={!r}, ".format(
1139+
self.scale_factor_at_projection_origin
11261140
)
1141+
return (
1142+
f"(central_lat={self.central_lat}, central_lon={self.central_lon}, "
1143+
f"false_easting={self.false_easting}, false_northing={self.false_northing}, "
1144+
f"{scale_info}"
1145+
f"ellipsoid={self.ellipsoid})"
11271146
)
11281147

1148+
def __repr__(self):
1149+
return "Stereographic" + self._repr_attributes()
1150+
11291151
def as_cartopy_crs(self):
11301152
globe = self._ellipsoid_to_globe(self.ellipsoid, ccrs.Globe())
11311153

@@ -1135,13 +1157,81 @@ def as_cartopy_crs(self):
11351157
self.false_easting,
11361158
self.false_northing,
11371159
self.true_scale_lat,
1160+
self.scale_factor_at_projection_origin,
11381161
globe=globe,
11391162
)
11401163

11411164
def as_cartopy_projection(self):
11421165
return self.as_cartopy_crs()
11431166

11441167

1168+
class PolarStereographic(Stereographic):
1169+
"""
1170+
A subclass of the stereographic map projection centred on a pole.
1171+
1172+
"""
1173+
1174+
grid_mapping_name = "polar_stereographic"
1175+
1176+
def __init__(
1177+
self,
1178+
central_lat,
1179+
central_lon,
1180+
false_easting=None,
1181+
false_northing=None,
1182+
true_scale_lat=None,
1183+
scale_factor_at_projection_origin=None,
1184+
ellipsoid=None,
1185+
):
1186+
"""
1187+
Construct a Polar Stereographic coord system.
1188+
1189+
Parameters
1190+
----------
1191+
1192+
central_lat : {90, -90}
1193+
The latitude of the pole.
1194+
1195+
central_lon : float
1196+
The central longitude, which aligns with the y axis.
1197+
1198+
false_easting : float, optional
1199+
X offset from planar origin in metres.
1200+
1201+
false_northing : float, optional
1202+
Y offset from planar origin in metres.
1203+
1204+
true_scale_lat : float, optional
1205+
Latitude of true scale.
1206+
1207+
scale_factor_at_projection_origin : float, optional
1208+
Scale factor at the origin of the projection
1209+
1210+
ellipsoid : :class:`GeogCS`, optional
1211+
If given, defines the ellipsoid.
1212+
1213+
Notes
1214+
-----
1215+
It is only valid to provide at most one of `true_scale_lat` and
1216+
`scale_factor_at_projection_origin`.
1217+
1218+
1219+
"""
1220+
1221+
super().__init__(
1222+
central_lat=central_lat,
1223+
central_lon=central_lon,
1224+
false_easting=false_easting,
1225+
false_northing=false_northing,
1226+
true_scale_lat=true_scale_lat,
1227+
scale_factor_at_projection_origin=scale_factor_at_projection_origin,
1228+
ellipsoid=ellipsoid,
1229+
)
1230+
1231+
def __repr__(self):
1232+
return "PolarStereographic" + self._repr_attributes()
1233+
1234+
11451235
class LambertConformal(CoordSystem):
11461236
"""
11471237
A coordinate system in the Lambert Conformal conic projection.

lib/iris/fileformats/_nc_load_rules/actions.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,13 @@ def action_default(engine):
110110
hh.build_transverse_mercator_coordinate_system,
111111
),
112112
hh.CF_GRID_MAPPING_STEREO: (
113-
hh.has_supported_stereographic_parameters,
113+
None,
114114
hh.build_stereographic_coordinate_system,
115115
),
116+
hh.CF_GRID_MAPPING_POLAR: (
117+
hh.has_supported_polar_stereographic_parameters,
118+
hh.build_polar_stereographic_coordinate_system,
119+
),
116120
hh.CF_GRID_MAPPING_LAMBERT_CONFORMAL: (
117121
None,
118122
hh.build_lambert_conformal_coordinate_system,

lib/iris/fileformats/_nc_load_rules/helpers.py

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
CF_ATTR_GRID_SEMI_MINOR_AXIS = "semi_minor_axis"
146146
CF_ATTR_GRID_LAT_OF_PROJ_ORIGIN = "latitude_of_projection_origin"
147147
CF_ATTR_GRID_LON_OF_PROJ_ORIGIN = "longitude_of_projection_origin"
148+
CF_ATTR_GRID_STRAIGHT_VERT_LON = "straight_vertical_longitude_from_pole"
148149
CF_ATTR_GRID_STANDARD_PARALLEL = "standard_parallel"
149150
CF_ATTR_GRID_FALSE_EASTING = "false_easting"
150151
CF_ATTR_GRID_FALSE_NORTHING = "false_northing"
@@ -418,8 +419,6 @@ def build_stereographic_coordinate_system(engine, cf_grid_var):
418419
)
419420
false_easting = getattr(cf_grid_var, CF_ATTR_GRID_FALSE_EASTING, None)
420421
false_northing = getattr(cf_grid_var, CF_ATTR_GRID_FALSE_NORTHING, None)
421-
# Iris currently only supports Stereographic projections with a scale
422-
# factor of 1.0. This is checked elsewhere.
423422

424423
cs = iris.coord_systems.Stereographic(
425424
latitude_of_projection_origin,
@@ -433,6 +432,42 @@ def build_stereographic_coordinate_system(engine, cf_grid_var):
433432
return cs
434433

435434

435+
################################################################################
436+
def build_polar_stereographic_coordinate_system(engine, cf_grid_var):
437+
"""
438+
Create a polar stereographic coordinate system from the CF-netCDF
439+
grid mapping variable.
440+
441+
"""
442+
ellipsoid = _get_ellipsoid(cf_grid_var)
443+
444+
latitude_of_projection_origin = getattr(
445+
cf_grid_var, CF_ATTR_GRID_LAT_OF_PROJ_ORIGIN, None
446+
)
447+
longitude_of_projection_origin = getattr(
448+
cf_grid_var, CF_ATTR_GRID_STRAIGHT_VERT_LON, None
449+
)
450+
true_scale_lat = getattr(cf_grid_var, CF_ATTR_GRID_STANDARD_PARALLEL, None)
451+
scale_factor_at_projection_origin = getattr(
452+
cf_grid_var, CF_ATTR_GRID_SCALE_FACTOR_AT_PROJ_ORIGIN, None
453+
)
454+
455+
false_easting = getattr(cf_grid_var, CF_ATTR_GRID_FALSE_EASTING, None)
456+
false_northing = getattr(cf_grid_var, CF_ATTR_GRID_FALSE_NORTHING, None)
457+
458+
cs = iris.coord_systems.PolarStereographic(
459+
latitude_of_projection_origin,
460+
longitude_of_projection_origin,
461+
false_easting,
462+
false_northing,
463+
true_scale_lat,
464+
scale_factor_at_projection_origin,
465+
ellipsoid=ellipsoid,
466+
)
467+
468+
return cs
469+
470+
436471
################################################################################
437472
def build_mercator_coordinate_system(engine, cf_grid_var):
438473
"""
@@ -1239,24 +1274,45 @@ def has_supported_mercator_parameters(engine, cf_name):
12391274

12401275

12411276
################################################################################
1242-
def has_supported_stereographic_parameters(engine, cf_name):
1243-
"""Determine whether the CF grid mapping variable has a value of 1.0
1244-
for the scale_factor_at_projection_origin attribute."""
1277+
def has_supported_polar_stereographic_parameters(engine, cf_name):
1278+
"""Determine whether the CF grid mapping variable has the supported
1279+
values for the parameters of the Polar Stereographic projection."""
12451280

12461281
is_valid = True
12471282
cf_grid_var = engine.cf_var.cf_group[cf_name]
12481283

1284+
latitude_of_projection_origin = getattr(
1285+
cf_grid_var, CF_ATTR_GRID_LAT_OF_PROJ_ORIGIN, None
1286+
)
1287+
1288+
standard_parallel = getattr(
1289+
cf_grid_var, CF_ATTR_GRID_STANDARD_PARALLEL, None
1290+
)
12491291
scale_factor_at_projection_origin = getattr(
12501292
cf_grid_var, CF_ATTR_GRID_SCALE_FACTOR_AT_PROJ_ORIGIN, None
12511293
)
12521294

1295+
if (
1296+
latitude_of_projection_origin != 90
1297+
and latitude_of_projection_origin != -90
1298+
):
1299+
warnings.warn('"latitude_of_projection_origin" must be +90 or -90.')
1300+
is_valid = False
1301+
12531302
if (
12541303
scale_factor_at_projection_origin is not None
1255-
and scale_factor_at_projection_origin != 1
1304+
and standard_parallel is not None
12561305
):
12571306
warnings.warn(
1258-
"Scale factors other than 1.0 not yet supported for "
1259-
"stereographic projections"
1307+
"It does not make sense to provide both "
1308+
'"scale_factor_at_projection_origin" and "standard_parallel".'
1309+
)
1310+
is_valid = False
1311+
1312+
if scale_factor_at_projection_origin is None and standard_parallel is None:
1313+
warnings.warn(
1314+
'One of "scale_factor_at_projection_origin" and '
1315+
'"standard_parallel" is required.'
12601316
)
12611317
is_valid = False
12621318

lib/iris/fileformats/netcdf.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2685,27 +2685,46 @@ def add_ellipsoid(ellipsoid):
26852685
cf_var_grid.false_easting = cs.false_easting
26862686
cf_var_grid.false_northing = cs.false_northing
26872687

2688-
# stereo
2689-
elif isinstance(cs, iris.coord_systems.Stereographic):
2688+
# polar stereo (have to do this before Stereographic because it subclasses it)
2689+
elif isinstance(cs, iris.coord_systems.PolarStereographic):
2690+
if cs.ellipsoid:
2691+
add_ellipsoid(cs.ellipsoid)
2692+
cf_var_grid.latitude_of_projection_origin = cs.central_lat
2693+
cf_var_grid.straight_vertical_longitude_from_pole = (
2694+
cs.central_lon
2695+
)
2696+
cf_var_grid.false_easting = cs.false_easting
2697+
cf_var_grid.false_northing = cs.false_northing
2698+
# Only one of these should be set
26902699
if cs.true_scale_lat is not None:
2691-
warnings.warn(
2692-
"Stereographic coordinate systems with "
2693-
"true scale latitude specified are not "
2694-
"yet handled"
2700+
cf_var_grid.true_scale_lat = cs.true_scale_lat
2701+
elif cs.scale_factor_at_projection_origin is not None:
2702+
cf_var_grid.scale_factor_at_projection_origin = (
2703+
cs.scale_factor_at_projection_origin
26952704
)
26962705
else:
2697-
if cs.ellipsoid:
2698-
add_ellipsoid(cs.ellipsoid)
2699-
cf_var_grid.longitude_of_projection_origin = (
2700-
cs.central_lon
2706+
cf_var_grid.scale_factor_at_projection_origin = 1.0
2707+
2708+
# stereo
2709+
elif isinstance(cs, iris.coord_systems.Stereographic):
2710+
if cs.ellipsoid:
2711+
add_ellipsoid(cs.ellipsoid)
2712+
cf_var_grid.longitude_of_projection_origin = cs.central_lon
2713+
cf_var_grid.latitude_of_projection_origin = cs.central_lat
2714+
cf_var_grid.false_easting = cs.false_easting
2715+
cf_var_grid.false_northing = cs.false_northing
2716+
# Only one of these should be set
2717+
if cs.true_scale_lat is not None:
2718+
msg = (
2719+
"It is not valid CF to save a true_scale_lat for "
2720+
"a Stereographic grid mapping."
27012721
)
2702-
cf_var_grid.latitude_of_projection_origin = (
2703-
cs.central_lat
2722+
raise ValueError(msg)
2723+
elif cs.scale_factor_at_projection_origin is not None:
2724+
cf_var_grid.scale_factor_at_projection_origin = (
2725+
cs.scale_factor_at_projection_origin
27042726
)
2705-
cf_var_grid.false_easting = cs.false_easting
2706-
cf_var_grid.false_northing = cs.false_northing
2707-
# The Stereographic class has an implicit scale
2708-
# factor
2727+
else:
27092728
cf_var_grid.scale_factor_at_projection_origin = 1.0
27102729

27112730
# osgb (a specific tmerc)

0 commit comments

Comments
 (0)