From c3e761ce73b8aa9cc48b267feed28804eb5a82e5 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 13 Oct 2023 16:20:45 +0100 Subject: [PATCH 01/33] Introduce new coord system classes. --- lib/iris/coord_systems.py | 178 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index e2003d1286..3fb58bdf18 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -1634,3 +1634,181 @@ def as_cartopy_crs(self): def as_cartopy_projection(self): return self.as_cartopy_crs() + + +class ObliqueMercator(CoordSystem): + """ + A cylindrical map projection, with XY coordinates measured in metres. + + Designed for regions not well suited to :class:`Mercator` or + :class:`TransverseMercator`, as the positioning of the cylinder is more + customisable. + + See Also + -------- + :class:`RotatedMercator` + + """ + + grid_mapping_name = "oblique_mercator" + + def __init__( + self, + azimuth_of_central_line, + latitude_of_projection_origin, + longitude_of_projection_origin, + false_easting=None, + false_northing=None, + scale_factor_at_central_meridian=None, + ellipsoid=None, + ): + """ + Constructs an ObliqueMercator object. + + Parameters + ---------- + azimuth_of_central_line : float + Azimuth of centerline clockwise from north at the center point of + the centre line. + latitude_of_projection_origin : float + The true longitude of the central meridian in degrees. + longitude_of_projection_origin: float + The true latitude of the planar origin in degrees. + false_easting: float, optional + X offset from the planar origin in metres. + Defaults to 0.0 . + false_northing: float, optional + Y offset from the planar origin in metres. + Defaults to 0.0 . + scale_factor_at_central_meridian: float, optional + Reduces the cylinder to slice through the ellipsoid (secant form). + Used to provide TWO longitudes of zero distortion in the area of + interest. + Defaults to 1.0 . + ellipsoid: :class:`GeogCS`, optional + If given, defines the ellipsoid. + + Examples + -------- + >>> from iris.coord_systems import GeogCS, ObliqueMercator + >>> my_ellipsoid = GeogCS(6371229.0, None, 0.0) + >>> ObliqueMercator(90.0, -22.0, -59.0, -25000.0, -25000.0, 1., my_ellipsoid) + ObliqueMercator(azimuth_of_central_line=90.0, latitude_of_projection_origin=-22.0, longitude_of_projection_origin=-59.0, false_easting=-25000.0, false_northing=-25000.0, scale_factor_at_central_meridian=1.0, ellipsoid=GeogCS(6371229.0)) + + """ + #: Azimuth of centerline clockwise from north. + self.azimuth_of_central_line = float(azimuth_of_central_line) + + #: True latitude of planar origin in degrees. + self.latitude_of_projection_origin = float( + latitude_of_projection_origin + ) + + #: True longitude of planar origin in degrees. + self.longitude_of_projection_origin = float( + longitude_of_projection_origin + ) + + #: X offset from planar origin in metres. + self.false_easting = _arg_default(false_easting, 0) + + #: Y offset from planar origin in metres. + self.false_northing = _arg_default(false_northing, 0) + + #: Scale factor at the centre longitude. + self.scale_factor_at_central_meridian = _arg_default( + scale_factor_at_central_meridian, 1.0 + ) + + #: Ellipsoid definition (:class:`GeogCS` or None). + self.ellipsoid = ellipsoid + + def __repr__(self): + return ( + "ObliqueMercator(azimuth_of_central_line={!r}, " + "latitude_of_projection_origin={!r}, " + "longitude_of_projection_origin={!r}, false_easting={!r}, " + "false_northing={!r}, scale_factor_at_central_meridian={!r}, " + "ellipsoid={!r})".format( + self.azimuth_of_central_line, + self.latitude_of_projection_origin, + self.longitude_of_projection_origin, + self.false_easting, + self.false_northing, + self.scale_factor_at_central_meridian, + self.ellipsoid, + ) + ) + + def as_cartopy_crs(self): + globe = self._ellipsoid_to_globe(self.ellipsoid, None) + + return ccrs.ObliqueMercator( + central_longitude=self.longitude_of_projection_origin, + central_latitude=self.latitude_of_projection_origin, + false_easting=self.false_easting, + false_northing=self.false_northing, + scale_factor=self.scale_factor_at_central_meridian, + azimuth=self.azimuth_of_central_line, + globe=globe, + ) + + def as_cartopy_projection(self): + return self.as_cartopy_crs() + + +class RotatedMercator(ObliqueMercator): + """ + :class:`ObliqueMercator` with ``azimuth_of_central_line=90``. + + As noted in CF: + + The Rotated Mercator projection is an Oblique Mercator projection + with azimuth = +90. + + """ + + grid_mapping_name = "rotated_mercator" + + def __init__( + self, + latitude_of_projection_origin, + longitude_of_projection_origin, + false_easting=None, + false_northing=None, + scale_factor_at_central_meridian=None, + ellipsoid=None, + ): + """ + Constructs a RotatedMercator object. + + Parameters + ---------- + latitude_of_projection_origin : float + The true longitude of the central meridian in degrees. + longitude_of_projection_origin: float + The true latitude of the planar origin in degrees. + false_easting: float, optional + X offset from the planar origin in metres. + Defaults to 0.0 . + false_northing: float, optional + Y offset from the planar origin in metres. + Defaults to 0.0 . + scale_factor_at_central_meridian: float, optional + Reduces the cylinder to slice through the ellipsoid (secant form). + Used to provide TWO longitudes of zero distortion in the area of + interest. + Defaults to 1.0 . + ellipsoid: :class:`GeogCS`, optional + If given, defines the ellipsoid. + + """ + super().__init__( + 90.0, + latitude_of_projection_origin, + longitude_of_projection_origin, + false_easting, + false_northing, + scale_factor_at_central_meridian, + ellipsoid, + ) From d60eab947f444b55ddd533ebbc63da323b03b838 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 13 Oct 2023 17:06:44 +0100 Subject: [PATCH 02/33] Add loading code for oblique mercator. --- lib/iris/coord_systems.py | 26 +++++---- .../fileformats/_nc_load_rules/actions.py | 8 +++ .../fileformats/_nc_load_rules/helpers.py | 53 +++++++++++++++++++ 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index 3fb58bdf18..7481f18a04 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -1659,7 +1659,7 @@ def __init__( longitude_of_projection_origin, false_easting=None, false_northing=None, - scale_factor_at_central_meridian=None, + scale_factor_at_projection_origin=None, ellipsoid=None, ): """ @@ -1680,10 +1680,8 @@ def __init__( false_northing: float, optional Y offset from the planar origin in metres. Defaults to 0.0 . - scale_factor_at_central_meridian: float, optional - Reduces the cylinder to slice through the ellipsoid (secant form). - Used to provide TWO longitudes of zero distortion in the area of - interest. + scale_factor_at_projection_origin: float, optional + TODO Defaults to 1.0 . ellipsoid: :class:`GeogCS`, optional If given, defines the ellipsoid. @@ -1693,7 +1691,7 @@ def __init__( >>> from iris.coord_systems import GeogCS, ObliqueMercator >>> my_ellipsoid = GeogCS(6371229.0, None, 0.0) >>> ObliqueMercator(90.0, -22.0, -59.0, -25000.0, -25000.0, 1., my_ellipsoid) - ObliqueMercator(azimuth_of_central_line=90.0, latitude_of_projection_origin=-22.0, longitude_of_projection_origin=-59.0, false_easting=-25000.0, false_northing=-25000.0, scale_factor_at_central_meridian=1.0, ellipsoid=GeogCS(6371229.0)) + ObliqueMercator(azimuth_of_central_line=90.0, latitude_of_projection_origin=-22.0, longitude_of_projection_origin=-59.0, false_easting=-25000.0, false_northing=-25000.0, scale_factor_at_projection_origin=1.0, ellipsoid=GeogCS(6371229.0)) """ #: Azimuth of centerline clockwise from north. @@ -1716,8 +1714,8 @@ def __init__( self.false_northing = _arg_default(false_northing, 0) #: Scale factor at the centre longitude. - self.scale_factor_at_central_meridian = _arg_default( - scale_factor_at_central_meridian, 1.0 + self.scale_factor_at_projection_origin = _arg_default( + scale_factor_at_projection_origin, 1.0 ) #: Ellipsoid definition (:class:`GeogCS` or None). @@ -1728,14 +1726,14 @@ def __repr__(self): "ObliqueMercator(azimuth_of_central_line={!r}, " "latitude_of_projection_origin={!r}, " "longitude_of_projection_origin={!r}, false_easting={!r}, " - "false_northing={!r}, scale_factor_at_central_meridian={!r}, " + "false_northing={!r}, scale_factor_at_projection_origin={!r}, " "ellipsoid={!r})".format( self.azimuth_of_central_line, self.latitude_of_projection_origin, self.longitude_of_projection_origin, self.false_easting, self.false_northing, - self.scale_factor_at_central_meridian, + self.scale_factor_at_projection_origin, self.ellipsoid, ) ) @@ -1748,7 +1746,7 @@ def as_cartopy_crs(self): central_latitude=self.latitude_of_projection_origin, false_easting=self.false_easting, false_northing=self.false_northing, - scale_factor=self.scale_factor_at_central_meridian, + scale_factor=self.scale_factor_at_projection_origin, azimuth=self.azimuth_of_central_line, globe=globe, ) @@ -1776,7 +1774,7 @@ def __init__( longitude_of_projection_origin, false_easting=None, false_northing=None, - scale_factor_at_central_meridian=None, + scale_factor_at_projection_origin=None, ellipsoid=None, ): """ @@ -1794,7 +1792,7 @@ def __init__( false_northing: float, optional Y offset from the planar origin in metres. Defaults to 0.0 . - scale_factor_at_central_meridian: float, optional + scale_factor_at_projection_origin: float, optional Reduces the cylinder to slice through the ellipsoid (secant form). Used to provide TWO longitudes of zero distortion in the area of interest. @@ -1809,6 +1807,6 @@ def __init__( longitude_of_projection_origin, false_easting, false_northing, - scale_factor_at_central_meridian, + scale_factor_at_projection_origin, ellipsoid, ) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index be84b65132..44ef7ac549 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -156,6 +156,14 @@ def action_default(engine): None, hh.build_geostationary_coordinate_system, ), + hh.CF_GRID_MAPPING_OBLIQUE: ( + None, + hh.build_oblique_mercator_coordinate_system, + ), + hh.CF_GRID_MAPPING_ROTATED_MERCATOR: ( + None, + hh.build_oblique_mercator_coordinate_system, + ), } diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 19a9cd18ca..be1b3aeef6 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -124,6 +124,8 @@ CF_GRID_MAPPING_TRANSVERSE = "transverse_mercator" CF_GRID_MAPPING_VERTICAL = "vertical_perspective" CF_GRID_MAPPING_GEOSTATIONARY = "geostationary" +CF_GRID_MAPPING_OBLIQUE = "oblique_mercator" +CF_GRID_MAPPING_ROTATED_MERCATOR = "rotated_mercator" # # CF Attribute Names. @@ -154,6 +156,7 @@ CF_ATTR_GRID_STANDARD_PARALLEL = "standard_parallel" CF_ATTR_GRID_PERSPECTIVE_HEIGHT = "perspective_point_height" CF_ATTR_GRID_SWEEP_ANGLE_AXIS = "sweep_angle_axis" +CF_ATTR_GRID_AZIMUTH_CENT_LINE = "azimuth_of_central_line" CF_ATTR_POSITIVE = "positive" CF_ATTR_STD_NAME = "standard_name" CF_ATTR_LONG_NAME = "long_name" @@ -893,6 +896,56 @@ def build_geostationary_coordinate_system(engine, cf_grid_var): return cs +################################################################################ +def build_oblique_mercator_coordinate_system(engine, cf_grid_var): + """ + Create an oblique mercator coordinate system from the CF-netCDF + grid mapping variable. + + """ + ellipsoid = _get_ellipsoid(cf_grid_var) + + azimuth_of_central_line = getattr( + cf_grid_var, CF_ATTR_GRID_AZIMUTH_CENT_LINE, None + ) + 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_LON_OF_PROJ_ORIGIN, 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) + kwargs = dict( + azimuth_of_central_line=azimuth_of_central_line, + latitude_of_projection_origin=latitude_of_projection_origin, + longitude_of_projection_origin=longitude_of_projection_origin, + scale_factor_at_projection_origin=scale_factor_at_projection_origin, + false_easting=false_easting, + false_northing=false_northing, + ellipsoid=ellipsoid, + ) + + # Handle the alternative form noted in CF: rotated mercator. + grid_mapping_name = getattr(cf_grid_var, CF_ATTR_GRID_MAPPING_NAME) + candidate_systems = { + cs.grid_mapping_name: cs + for cs in ( + iris.coord_systems.ObliqueMercator, + iris.coord_systems.RotatedMercator, + ) + } + SelectedSystem = candidate_systems[grid_mapping_name] + if not hasattr(SelectedSystem, CF_ATTR_GRID_AZIMUTH_CENT_LINE): + del kwargs[CF_ATTR_GRID_AZIMUTH_CENT_LINE] + + cs = SelectedSystem(**kwargs) + return cs + + ################################################################################ def get_attr_units(cf_var, attributes): attr_units = getattr(cf_var, CF_ATTR_UNITS, UNKNOWN_UNIT_STRING) From 5ca387390519efe8faad2e01ada18d162a362e58 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 17 Oct 2023 15:32:10 +0100 Subject: [PATCH 03/33] Fix for azimuth check. --- lib/iris/fileformats/_nc_load_rules/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index be1b3aeef6..f637460d33 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -939,7 +939,7 @@ def build_oblique_mercator_coordinate_system(engine, cf_grid_var): ) } SelectedSystem = candidate_systems[grid_mapping_name] - if not hasattr(SelectedSystem, CF_ATTR_GRID_AZIMUTH_CENT_LINE): + if SelectedSystem is iris.coord_systems.RotatedMercator: del kwargs[CF_ATTR_GRID_AZIMUTH_CENT_LINE] cs = SelectedSystem(**kwargs) From 0176b1de78044c255bf785d2dd526197b600e984 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 17 Oct 2023 16:06:09 +0100 Subject: [PATCH 04/33] Add saving code for oblique mercator. --- lib/iris/fileformats/netcdf/saver.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 1ff69df1f7..f519746924 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2210,6 +2210,32 @@ def add_ellipsoid(ellipsoid): ) cf_var_grid.sweep_angle_axis = cs.sweep_angle_axis + # oblique mercator (and rotated variant) + elif isinstance( + cs, + ( + iris.coord_systems.ObliqueMercator, + iris.coord_systems.RotatedMercator, + ), + ): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + if isinstance(cs, iris.coord_systems.ObliqueMercator): + cf_var_grid.azimuth_of_central_line = ( + cs.azimuth_of_central_line + ) + cf_var_grid.latitude_of_projection_origin = ( + cs.latitude_of_projection_origin + ) + cf_var_grid.longitude_of_projection_origin = ( + cs.longitude_of_projection_origin + ) + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + cf_var_grid.scale_factor_at_projection_origin = ( + cs.scale_factor_at_projection_origin + ) + # other else: warnings.warn( From af074a98a80407593e226f03a0ee2da270736872 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 17 Oct 2023 17:06:49 +0100 Subject: [PATCH 05/33] Fix to rotated repr. --- lib/iris/coord_systems.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index 7481f18a04..ab72463a43 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -10,6 +10,7 @@ from abc import ABCMeta, abstractmethod from functools import cached_property +import re import warnings import cartopy.crs as ccrs @@ -1723,11 +1724,12 @@ def __init__( def __repr__(self): return ( - "ObliqueMercator(azimuth_of_central_line={!r}, " + "{!s}(azimuth_of_central_line={!r}, " "latitude_of_projection_origin={!r}, " "longitude_of_projection_origin={!r}, false_easting={!r}, " "false_northing={!r}, scale_factor_at_projection_origin={!r}, " "ellipsoid={!r})".format( + self.__class__.__name__, self.azimuth_of_central_line, self.latitude_of_projection_origin, self.longitude_of_projection_origin, @@ -1810,3 +1812,9 @@ def __init__( scale_factor_at_projection_origin, ellipsoid, ) + + def __repr__(self): + # Remove the azimuth argument from the parent repr. + result = super().__repr__() + result = re.sub(r"azimuth_of_central_line=\d*\.?\d*, ", "", result) + return result From 518200a05ea89f28d2780040d601577ca9f7c2bf Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 17 Oct 2023 17:21:11 +0100 Subject: [PATCH 06/33] Scale factor wording fix. --- lib/iris/coord_systems.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index ab72463a43..fb90bd25c2 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -1682,7 +1682,7 @@ def __init__( Y offset from the planar origin in metres. Defaults to 0.0 . scale_factor_at_projection_origin: float, optional - TODO + Scale factor at the central meridian. Defaults to 1.0 . ellipsoid: :class:`GeogCS`, optional If given, defines the ellipsoid. @@ -1714,7 +1714,7 @@ def __init__( #: Y offset from planar origin in metres. self.false_northing = _arg_default(false_northing, 0) - #: Scale factor at the centre longitude. + #: Scale factor at the central meridian. self.scale_factor_at_projection_origin = _arg_default( scale_factor_at_projection_origin, 1.0 ) @@ -1795,9 +1795,7 @@ def __init__( Y offset from the planar origin in metres. Defaults to 0.0 . scale_factor_at_projection_origin: float, optional - Reduces the cylinder to slice through the ellipsoid (secant form). - Used to provide TWO longitudes of zero distortion in the area of - interest. + Scale factor at the central meridian. Defaults to 1.0 . ellipsoid: :class:`GeogCS`, optional If given, defines the ellipsoid. From 7a74af9d1f131a1e99b1f993c545571a15ee31d6 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 20 Oct 2023 17:39:32 +0100 Subject: [PATCH 07/33] Tests first pass. --- .../coord_systems/test_ObliqueMercator.py | 159 ++++++++++++++++++ .../coord_systems/test_RotatedMercator.py | 20 +++ 2 files changed, 179 insertions(+) create mode 100644 lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py create mode 100644 lib/iris/tests/unit/coord_systems/test_RotatedMercator.py diff --git a/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py b/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py new file mode 100644 index 0000000000..b25e36d46e --- /dev/null +++ b/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py @@ -0,0 +1,159 @@ +# 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.ObliqueMercator` class.""" + +from typing import List, NamedTuple +from unittest.mock import Mock + +from cartopy import crs as ccrs +import pytest + +from iris.coord_systems import GeogCS, ObliqueMercator + +#### +# ALL TESTS MUST BE CONTAINED IN CLASSES, TO ENABLE INHERITANCE BY +# test_RotatedMercator.py . +#### + + +class TestArgs: + GeogCS = GeogCS + class_kwargs_default = dict( + azimuth_of_central_line=0.0, + latitude_of_projection_origin=0.0, + longitude_of_projection_origin=0.0, + ) + cartopy_kwargs_default = dict( + central_longitude=0.0, + central_latitude=0.0, + false_easting=0.0, + false_northing=0.0, + scale_factor=1.0, + azimuth=0.0, + globe=None, + ) + + class GlobeWithEq(ccrs.Globe): + def __eq__(self, other): + """Need eq to enable comparison with expected arguments.""" + result = NotImplemented + if isinstance(other, ccrs.Globe): + result = other.__dict__ == self.__dict__ + return result + + class ParamTuple(NamedTuple): + id: str + class_kwargs: dict + cartopy_kwargs: dict + + param_list: List[ParamTuple] = [ + ParamTuple( + "default", + dict(), + dict(), + ), + ParamTuple( + "azimuth", + dict(azimuth_of_central_line=90), + dict(azimuth=90), + ), + ParamTuple( + "central_longitude", + dict(longitude_of_projection_origin=90), + dict(central_longitude=90), + ), + ParamTuple( + "central_latitude", + dict(latitude_of_projection_origin=45), + dict(central_latitude=45), + ), + ParamTuple( + "false_easting_northing", + dict(false_easting=1000000, false_northing=-2000000), + dict(false_easting=1000000, false_northing=-2000000), + ), + ParamTuple( + "scale_factor", + # Number inherited from Cartopy's test_mercator.py . + dict(scale_factor_at_projection_origin=0.939692620786), + dict(scale_factor=0.939692620786), + ), + ParamTuple( + "globe", + dict(ellipsoid=GeogCS(1)), + dict( + globe=GlobeWithEq( + semimajor_axis=1, semiminor_axis=1, ellipse=None + ) + ), + ), + ParamTuple( + "combo", + dict( + azimuth_of_central_line=90, + longitude_of_projection_origin=90, + latitude_of_projection_origin=45, + false_easting=1000000, + false_northing=-2000000, + scale_factor_at_projection_origin=0.939692620786, + ellipsoid=GeogCS(1), + ), + dict( + azimuth=90.0, + central_longitude=90.0, + central_latitude=45.0, + false_easting=1000000, + false_northing=-2000000, + scale_factor=0.939692620786, + globe=GlobeWithEq( + semimajor_axis=1, semiminor_axis=1, ellipse=None + ), + ), + ), + ] + param_ids: List[str] = [p.id for p in param_list] + + @pytest.fixture(autouse=True, params=param_list, ids=param_ids) + def make_variant_inputs(self, request) -> None: + inputs: TestArgs.ParamTuple = request.param + self.class_kwargs = dict( + self.class_kwargs_default, **inputs.class_kwargs + ) + self.cartopy_kwargs_expected = dict( + self.cartopy_kwargs_default, **inputs.cartopy_kwargs + ) + + def make_instance(self) -> ObliqueMercator: + return ObliqueMercator(**self.class_kwargs) + + @pytest.fixture() + def instance(self): + yield self.make_instance() + + def test_instantiate(self): + _ = self.make_instance() + + def test_cartopy_crs(self, instance): + ccrs.ObliqueMercator = Mock() + instance.as_cartopy_crs() + ccrs.ObliqueMercator.assert_called_with(**self.cartopy_kwargs_expected) + + def test_cartopy_projection(self, instance): + ccrs.ObliqueMercator = Mock() + instance.as_cartopy_projection() + ccrs.ObliqueMercator.assert_called_with(**self.cartopy_kwargs_expected) + + @pytest.fixture() + def label_class(self, instance): + """Make the tested coordinate system available, even for subclasses.""" + from iris import coord_systems + + instance_class = "{!s}".format(instance.__class__.__name__) + globals()[instance_class] = getattr(coord_systems, instance_class) + + def test_repr(self, instance, label_class): + """Test that the repr can be used to regenerate an identical object.""" + assert eval(repr(instance)) == instance diff --git a/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py b/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py new file mode 100644 index 0000000000..545f628a56 --- /dev/null +++ b/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py @@ -0,0 +1,20 @@ +# 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.RotatedMercator` class.""" + +from iris.coord_systems import RotatedMercator + +from . import test_ObliqueMercator + + +class TestArgs(test_ObliqueMercator.TestArgs): + def make_instance(self) -> RotatedMercator: + kwargs = self.class_kwargs + kwargs.pop("azimuth_of_central_line", None) + return RotatedMercator(**kwargs) + + +TestArgs.cartopy_kwargs_default["azimuth"] = 90.0 From a81507c59f4a873ca32e8aa683f91e3215e09ff9 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 10:46:47 +0100 Subject: [PATCH 08/33] Temp test disable. --- .github/workflows/ci-manifest.yml | 6 +++--- .github/workflows/ci-tests.yml | 16 ++++++++-------- .github/workflows/ci-wheels.yml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-manifest.yml b/.github/workflows/ci-manifest.yml index 391f944310..f7ce871950 100644 --- a/.github/workflows/ci-manifest.yml +++ b/.github/workflows/ci-manifest.yml @@ -4,9 +4,9 @@ name: ci-manifest on: - pull_request: - branches: - - "*" +# pull_request: +# branches: +# - "*" push: branches-ignore: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 8d84d4e137..58169c1108 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,19 +35,19 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest"] - python-version: ["3.11"] - session: ["doctest", "gallery", "linkcheck"] +# python-version: ["3.11"] +# session: ["doctest", "gallery", "linkcheck"] include: - os: "ubuntu-latest" python-version: "3.11" session: "tests" coverage: "--coverage" - - os: "ubuntu-latest" - python-version: "3.10" - session: "tests" - - os: "ubuntu-latest" - python-version: "3.9" - session: "tests" +# - os: "ubuntu-latest" +# python-version: "3.10" +# session: "tests" +# - os: "ubuntu-latest" +# python-version: "3.9" +# session: "tests" env: IRIS_TEST_DATA_VERSION: "2.21" diff --git a/.github/workflows/ci-wheels.yml b/.github/workflows/ci-wheels.yml index 942d528f6d..ea0b0bb892 100644 --- a/.github/workflows/ci-wheels.yml +++ b/.github/workflows/ci-wheels.yml @@ -9,7 +9,7 @@ name: ci-wheels on: - pull_request: +# pull_request: push: tags: From 27c486f3122a6056618b865c0534e7b871da9288 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 10:47:40 +0100 Subject: [PATCH 09/33] Temp RotatedMercator test disable. --- .../{test_RotatedMercator.py => disabled_RotatedMercator.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/iris/tests/unit/coord_systems/{test_RotatedMercator.py => disabled_RotatedMercator.py} (100%) diff --git a/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py b/lib/iris/tests/unit/coord_systems/disabled_RotatedMercator.py similarity index 100% rename from lib/iris/tests/unit/coord_systems/test_RotatedMercator.py rename to lib/iris/tests/unit/coord_systems/disabled_RotatedMercator.py From dd24604330a09dc89aa4b88f70a71e9872817c65 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 11:35:45 +0100 Subject: [PATCH 10/33] Deprecate RotatedMercator. --- lib/iris/coord_systems.py | 20 ++++++++++++++++--- .../fileformats/_nc_load_rules/helpers.py | 7 +++++++ lib/iris/fileformats/netcdf/saver.py | 14 +++++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index fb90bd25c2..3d986fefce 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -16,6 +16,7 @@ import cartopy.crs as ccrs import numpy as np +from iris._deprecation import warn_deprecated import iris.exceptions @@ -1761,14 +1762,19 @@ class RotatedMercator(ObliqueMercator): """ :class:`ObliqueMercator` with ``azimuth_of_central_line=90``. - As noted in CF: + As noted in CF versions 1.10 and earlier: The Rotated Mercator projection is an Oblique Mercator projection with azimuth = +90. - """ + .. deprecated:: 3.8.0 + This coordinate system was introduced as already scheduled for removal + in a future release, since CF version 1.11 onwards now requires use of + :class:`ObliqueMercator` with ``azimuth_of_central_line=90.`` . + Any :class:`RotatedMercator` instances will always be saved to NetCDF + as the ``oblique_mercator`` grid mapping. - grid_mapping_name = "rotated_mercator" + """ def __init__( self, @@ -1801,6 +1807,14 @@ def __init__( If given, defines the ellipsoid. """ + message = ( + "iris.coord_systems.RotatedMercator is deprecated, and will be " + "removed in a future release. Instead please use " + "iris.coord_systems.ObliqueMercator with " + "azimuth_of_central_line=90 ." + ) + warn_deprecated(message) + super().__init__( 90.0, latitude_of_projection_origin, diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index f637460d33..17bae075cf 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -23,6 +23,7 @@ import pyproj import iris +from iris._deprecation import warn_deprecated import iris.aux_factory from iris.common.mixin import _get_valid_standard_name import iris.coord_systems @@ -940,6 +941,12 @@ def build_oblique_mercator_coordinate_system(engine, cf_grid_var): } SelectedSystem = candidate_systems[grid_mapping_name] if SelectedSystem is iris.coord_systems.RotatedMercator: + message = ( + "Iris will stop loading the rotated_mercator grid mapping name in " + "a future release, in accordance with CF version 1.11 . Instead " + "please use oblique_mercator with azimuth_of_central_line = 90 ." + ) + warn_deprecated(message) del kwargs[CF_ATTR_GRID_AZIMUTH_CENT_LINE] cs = SelectedSystem(**kwargs) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index f519746924..5e386c2f88 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -102,6 +102,9 @@ # UKMO specific attributes that should not be global. _UKMO_DATA_ATTRS = ["STASH", "um_stash_source", "ukmo__process_flags"] +# TODO: whenever we advance to CF-1.11 we should then discuss a completion date +# for the deprecation of Rotated Mercator in coord_systems.py and +# _nc_load_rules/helpers.py . CF_CONVENTIONS_VERSION = "CF-1.7" _FactoryDefn = collections.namedtuple( @@ -2218,12 +2221,15 @@ def add_ellipsoid(ellipsoid): iris.coord_systems.RotatedMercator, ), ): + # RotatedMercator subclasses ObliqueMercator, and RM + # instances are implicitly saved as OM due to inherited + # properties. This is correct because CF 1.11 is removing + # all mention of RM. if cs.ellipsoid: add_ellipsoid(cs.ellipsoid) - if isinstance(cs, iris.coord_systems.ObliqueMercator): - cf_var_grid.azimuth_of_central_line = ( - cs.azimuth_of_central_line - ) + cf_var_grid.azimuth_of_central_line = ( + cs.azimuth_of_central_line + ) cf_var_grid.latitude_of_projection_origin = ( cs.latitude_of_projection_origin ) From b2b6a11a2e917345aafa7a57ea3266714032c028 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 11:38:16 +0100 Subject: [PATCH 11/33] Revert "Temp RotatedMercator test disable." This reverts commit 27c486f3122a6056618b865c0534e7b871da9288. --- .../{disabled_RotatedMercator.py => test_RotatedMercator.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/iris/tests/unit/coord_systems/{disabled_RotatedMercator.py => test_RotatedMercator.py} (100%) diff --git a/lib/iris/tests/unit/coord_systems/disabled_RotatedMercator.py b/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py similarity index 100% rename from lib/iris/tests/unit/coord_systems/disabled_RotatedMercator.py rename to lib/iris/tests/unit/coord_systems/test_RotatedMercator.py From 687ccd22042d9db21499e415cba56bb8af7d141e Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 11:43:07 +0100 Subject: [PATCH 12/33] First attempted fix for RM test inheritance. --- .../unit/coord_systems/test_RotatedMercator.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py b/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py index 545f628a56..1011dc9c94 100644 --- a/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py +++ b/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py @@ -11,10 +11,21 @@ class TestArgs(test_ObliqueMercator.TestArgs): + class_kwargs_default = dict( + latitude_of_projection_origin=0.0, + longitude_of_projection_origin=0.0, + ) + cartopy_kwargs_default = dict( + central_longitude=0.0, + central_latitude=0.0, + false_easting=0.0, + false_northing=0.0, + scale_factor=1.0, + azimuth=90.0, + globe=None, + ) + def make_instance(self) -> RotatedMercator: kwargs = self.class_kwargs kwargs.pop("azimuth_of_central_line", None) return RotatedMercator(**kwargs) - - -TestArgs.cartopy_kwargs_default["azimuth"] = 90.0 From a01eab27da2f8571465e93238d380145db5511c2 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 11:58:43 +0100 Subject: [PATCH 13/33] Revert "Temp test disable." This reverts commit a81507c59f4a873ca32e8aa683f91e3215e09ff9. --- .github/workflows/ci-manifest.yml | 6 +++--- .github/workflows/ci-tests.yml | 16 ++++++++-------- .github/workflows/ci-wheels.yml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-manifest.yml b/.github/workflows/ci-manifest.yml index f7ce871950..391f944310 100644 --- a/.github/workflows/ci-manifest.yml +++ b/.github/workflows/ci-manifest.yml @@ -4,9 +4,9 @@ name: ci-manifest on: -# pull_request: -# branches: -# - "*" + pull_request: + branches: + - "*" push: branches-ignore: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 58169c1108..8d84d4e137 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,19 +35,19 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest"] -# python-version: ["3.11"] -# session: ["doctest", "gallery", "linkcheck"] + python-version: ["3.11"] + session: ["doctest", "gallery", "linkcheck"] include: - os: "ubuntu-latest" python-version: "3.11" session: "tests" coverage: "--coverage" -# - os: "ubuntu-latest" -# python-version: "3.10" -# session: "tests" -# - os: "ubuntu-latest" -# python-version: "3.9" -# session: "tests" + - os: "ubuntu-latest" + python-version: "3.10" + session: "tests" + - os: "ubuntu-latest" + python-version: "3.9" + session: "tests" env: IRIS_TEST_DATA_VERSION: "2.21" diff --git a/.github/workflows/ci-wheels.yml b/.github/workflows/ci-wheels.yml index ea0b0bb892..942d528f6d 100644 --- a/.github/workflows/ci-wheels.yml +++ b/.github/workflows/ci-wheels.yml @@ -9,7 +9,7 @@ name: ci-wheels on: -# pull_request: + pull_request: push: tags: From 249994596442dde1201cce3a1607f07b1b94cfe5 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 12:06:22 +0100 Subject: [PATCH 14/33] Fix warnings doctests. --- docs/src/further_topics/filtering_warnings.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/further_topics/filtering_warnings.rst b/docs/src/further_topics/filtering_warnings.rst index 689ea69a52..2cbad525d3 100644 --- a/docs/src/further_topics/filtering_warnings.rst +++ b/docs/src/further_topics/filtering_warnings.rst @@ -47,9 +47,9 @@ Warnings: >>> my_operation() ... - iris/coord_systems.py:454: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance. + iris/coord_systems.py:456: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance. warnings.warn(wmsg, category=iris.exceptions.IrisUserWarning) - iris/coord_systems.py:821: IrisDefaultingWarning: Discarding false_easting and false_northing that are not used by Cartopy. + iris/coord_systems.py:823: IrisDefaultingWarning: Discarding false_easting and false_northing that are not used by Cartopy. warnings.warn( Warnings can be suppressed using the Python warnings filter with the ``ignore`` @@ -110,7 +110,7 @@ You can target specific Warning messages, e.g. ... warnings.filterwarnings("ignore", message="Discarding false_easting") ... my_operation() ... - iris/coord_systems.py:454: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance. + iris/coord_systems.py:456: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance. warnings.warn(wmsg, category=iris.exceptions.IrisUserWarning) :: @@ -125,10 +125,10 @@ Or you can target Warnings raised by specific lines of specific modules, e.g. .. doctest:: filtering_warnings >>> with warnings.catch_warnings(): - ... warnings.filterwarnings("ignore", module="iris.coord_systems", lineno=454) + ... warnings.filterwarnings("ignore", module="iris.coord_systems", lineno=456) ... my_operation() ... - iris/coord_systems.py:821: IrisDefaultingWarning: Discarding false_easting and false_northing that are not used by Cartopy. + iris/coord_systems.py:823: IrisDefaultingWarning: Discarding false_easting and false_northing that are not used by Cartopy. warnings.warn( :: @@ -188,7 +188,7 @@ module during execution: ... ) ... my_operation() ... - iris/coord_systems.py:454: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance. + iris/coord_systems.py:456: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance. warnings.warn(wmsg, category=iris.exceptions.IrisUserWarning) ---- From 709c92eb181981e4429c892f31632a18566fb898 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 12:13:45 +0100 Subject: [PATCH 15/33] Add deprecation test for RotatedMercator. --- lib/iris/tests/unit/coord_systems/test_RotatedMercator.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py b/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py index 1011dc9c94..97921efec6 100644 --- a/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py +++ b/lib/iris/tests/unit/coord_systems/test_RotatedMercator.py @@ -5,6 +5,9 @@ # licensing details. """Unit tests for the :class:`iris.coord_systems.RotatedMercator` class.""" +import pytest + +from iris._deprecation import IrisDeprecation from iris.coord_systems import RotatedMercator from . import test_ObliqueMercator @@ -29,3 +32,8 @@ def make_instance(self) -> RotatedMercator: kwargs = self.class_kwargs kwargs.pop("azimuth_of_central_line", None) return RotatedMercator(**kwargs) + + +def test_deprecated(): + with pytest.warns(IrisDeprecation, match="azimuth_of_central_line=90"): + _ = RotatedMercator(0, 0) From e56a0a1c0b8faa22417417ca33c49293813206e4 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 15:02:43 +0100 Subject: [PATCH 16/33] Oblique Mercator loading tests. --- .../fileformats/_nc_load_rules/helpers.py | 11 +- ...uild_oblique_mercator_coordinate_system.py | 158 ++++++++++++++++++ 2 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 17bae075cf..374687b29f 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -932,13 +932,10 @@ def build_oblique_mercator_coordinate_system(engine, cf_grid_var): # Handle the alternative form noted in CF: rotated mercator. grid_mapping_name = getattr(cf_grid_var, CF_ATTR_GRID_MAPPING_NAME) - candidate_systems = { - cs.grid_mapping_name: cs - for cs in ( - iris.coord_systems.ObliqueMercator, - iris.coord_systems.RotatedMercator, - ) - } + candidate_systems = dict( + oblique_mercator=iris.coord_systems.ObliqueMercator, + rotated_mercator=iris.coord_systems.RotatedMercator, + ) SelectedSystem = candidate_systems[grid_mapping_name] if SelectedSystem is iris.coord_systems.RotatedMercator: message = ( diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py new file mode 100644 index 0000000000..9bd5a68c41 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py @@ -0,0 +1,158 @@ +# 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_oblique_mercator_coordinate_system`. + +""" +from typing import List, NamedTuple +from unittest import mock + +import pytest + +from iris import coord_systems +from iris.coord_systems import GeogCS, ObliqueMercator, RotatedMercator +from iris.fileformats._nc_load_rules.helpers import ( + build_oblique_mercator_coordinate_system, +) + + +class TestAttributes: + """Test that NetCDF attributes are correctly converted to class arguments.""" + + nc_attributes_default = dict( + grid_mapping_name="oblique_mercator", + azimuth_of_central_line=0.0, + latitude_of_projection_origin=0.0, + longitude_of_projection_origin=0.0, + scale_factor_at_projection_origin=1.0, + # Optional attributes not included. + ) + coord_system_kwargs_default = dict( + azimuth_of_central_line=0.0, + latitude_of_projection_origin=0.0, + longitude_of_projection_origin=0.0, + false_easting=None, + false_northing=None, + scale_factor_at_projection_origin=1.0, + ellipsoid=None, + ) + + class ParamTuple(NamedTuple): + id: str + nc_attributes: dict + expected_class: [ObliqueMercator, RotatedMercator] + coord_system_kwargs: dict + + param_list: List[ParamTuple] = [ + ParamTuple( + "default", + dict(), + ObliqueMercator, + dict(), + ), + ParamTuple( + "azimuth", + dict(azimuth_of_central_line=90), + ObliqueMercator, + dict(azimuth_of_central_line=90), + ), + ParamTuple( + "central_longitude", + dict(longitude_of_projection_origin=90), + ObliqueMercator, + dict(longitude_of_projection_origin=90), + ), + ParamTuple( + "central_latitude", + dict(latitude_of_projection_origin=45), + ObliqueMercator, + dict(latitude_of_projection_origin=45), + ), + ParamTuple( + "false_easting_northing", + dict(false_easting=1000000, false_northing=-2000000), + ObliqueMercator, + dict(false_easting=1000000, false_northing=-2000000), + ), + ParamTuple( + "scale_factor", + # Number inherited from Cartopy's test_mercator.py . + dict(scale_factor_at_projection_origin=0.939692620786), + ObliqueMercator, + dict(scale_factor_at_projection_origin=0.939692620786), + ), + ParamTuple( + "globe", + dict(semi_major_axis=1), + ObliqueMercator, + dict(ellipsoid=GeogCS(semi_major_axis=1, semi_minor_axis=1)), + ), + ParamTuple( + "combo", + dict( + azimuth_of_central_line=90, + longitude_of_projection_origin=90, + latitude_of_projection_origin=45, + false_easting=1000000, + false_northing=-2000000, + scale_factor_at_projection_origin=0.939692620786, + semi_major_axis=1, + ), + ObliqueMercator, + dict( + azimuth_of_central_line=90.0, + longitude_of_projection_origin=90.0, + latitude_of_projection_origin=45.0, + false_easting=1000000, + false_northing=-2000000, + scale_factor_at_projection_origin=0.939692620786, + ellipsoid=GeogCS(semi_major_axis=1, semi_minor_axis=1), + ), + ), + ParamTuple( + "rotated", + dict(grid_mapping_name="rotated_mercator"), + RotatedMercator, + dict(), + ), + ParamTuple( + "rotated_azimuth_ignored", + dict( + grid_mapping_name="rotated_mercator", + azimuth_of_central_line=45, + ), + RotatedMercator, + dict(), + ), + ] + param_ids: List[str] = [p.id for p in param_list] + + @pytest.fixture(autouse=True, params=param_list, ids=param_ids) + def make_variant_inputs(self, request) -> None: + inputs: TestAttributes.ParamTuple = request.param + + self.nc_attributes = dict( + self.nc_attributes_default, **inputs.nc_attributes + ) + self.expected_class = inputs.expected_class + coord_system_kwargs_expected = dict( + self.coord_system_kwargs_default, **inputs.coord_system_kwargs + ) + + if self.expected_class is RotatedMercator: + del coord_system_kwargs_expected["azimuth_of_central_line"] + + self.coord_system_args_expected = list( + coord_system_kwargs_expected.values() + ) + + def test_attributes(self): + cf_var_mock = mock.Mock(spec=[], **self.nc_attributes) + coord_system_mock = mock.Mock(spec=self.expected_class) + setattr(coord_systems, self.expected_class.__name__, coord_system_mock) + + _ = build_oblique_mercator_coordinate_system(None, cf_var_mock) + coord_system_mock.assert_called_with(*self.coord_system_args_expected) From 75878f66ad0f52e0dcc85d79e59395e865b96867 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 15:22:55 +0100 Subject: [PATCH 17/33] Oblique Mercator loading deprecation test. --- ...test_build_oblique_mercator_coordinate_system.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py index 9bd5a68c41..e9699cbf48 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py @@ -13,6 +13,7 @@ import pytest from iris import coord_systems +from iris._deprecation import IrisDeprecation from iris.coord_systems import GeogCS, ObliqueMercator, RotatedMercator from iris.fileformats._nc_load_rules.helpers import ( build_oblique_mercator_coordinate_system, @@ -156,3 +157,15 @@ def test_attributes(self): _ = build_oblique_mercator_coordinate_system(None, cf_var_mock) coord_system_mock.assert_called_with(*self.coord_system_args_expected) + + +def test_deprecation(): + nc_attributes = dict( + grid_mapping_name="rotated_mercator", + latitude_of_projection_origin=0.0, + longitude_of_projection_origin=0.0, + scale_factor_at_projection_origin=1.0, + ) + cf_var_mock = mock.Mock(spec=[], **nc_attributes) + with pytest.warns(IrisDeprecation, match="azimuth_of_central_line = 90"): + _ = build_oblique_mercator_coordinate_system(None, cf_var_mock) From c222712c978ac1f74e54bd44a8df8617b9a63985 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 16:14:17 +0100 Subject: [PATCH 18/33] Saving test for Oblique Mercator. --- .../fileformats/netcdf/saver/test_Saver.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py index af0d7bcd30..8253e59368 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py +++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py @@ -24,7 +24,9 @@ LambertAzimuthalEqualArea, LambertConformal, Mercator, + ObliqueMercator, RotatedGeogCS, + RotatedMercator, Stereographic, TransverseMercator, VerticalPerspective, @@ -1065,6 +1067,50 @@ def test_geo_cs(self): } self._test(coord_system, expected) + def test_oblique_cs(self): + # Some none-default settings to confirm all parameters are being + # handled. + + kwargs_rotated = dict( + latitude_of_projection_origin=90.0, + longitude_of_projection_origin=45.0, + false_easting=1000000.0, + false_northing=-2000000.0, + scale_factor_at_projection_origin=0.939692620786, + ellipsoid=GeogCS(1), + ) + + # Same as rotated, but with azimuth too. + oblique_azimuth = dict(azimuth_of_central_line=45.0) + kwargs_oblique = dict(kwargs_rotated, **oblique_azimuth) + + expected_rotated = dict( + # Automatically converted to oblique_mercator in line with CF 1.11 . + grid_mapping_name=b"oblique_mercator", + # Azimuth should be automatically populated. + azimuth_of_central_line=90.0, + **kwargs_rotated, + ) + # Convert the ellipsoid + expected_rotated.update( + dict( + earth_radius=expected_rotated.pop("ellipsoid").semi_major_axis, + longitude_of_prime_meridian=0.0, + ) + ) + + # Same as rotated, but different azimuth. + expected_oblique = dict(expected_rotated, **oblique_azimuth) + + oblique = ObliqueMercator(**kwargs_oblique) + rotated = RotatedMercator(**kwargs_rotated) + + for coord_system, expected in [ + (oblique, expected_oblique), + (rotated, expected_rotated), + ]: + self._test(coord_system, expected) + if __name__ == "__main__": tests.main() From cdf3ffd16f7e12479b8ea5363c5eba60d869b5ba Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 16:30:57 +0100 Subject: [PATCH 19/33] Fix isinstance() check. --- lib/iris/fileformats/netcdf/saver.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 5e386c2f88..fe8aca735b 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2214,12 +2214,15 @@ def add_ellipsoid(ellipsoid): cf_var_grid.sweep_angle_axis = cs.sweep_angle_axis # oblique mercator (and rotated variant) - elif isinstance( - cs, - ( - iris.coord_systems.ObliqueMercator, - iris.coord_systems.RotatedMercator, - ), + # Slightly odd isinstance() check to allow successful mocking. + elif any( + [ + isinstance(cs, cs_class) + for cs_class in ( + iris.coord_systems.ObliqueMercator, + iris.coord_systems.RotatedMercator, + ) + ], ): # RotatedMercator subclasses ObliqueMercator, and RM # instances are implicitly saved as OM due to inherited From a1005a52d82e2c4d1069ff0ac4f44fd1aafa6f2d Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 16:33:14 +0100 Subject: [PATCH 20/33] What's New entry. --- docs/src/whatsnew/latest.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index c5cd961679..78c404227e 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -34,6 +34,10 @@ This document explains the changes made to Iris for this release :class:`UserWarning`\s for richer filtering. The full index of sub-categories can be seen here: :mod:`iris.exceptions` . (:pull:`5498`) +#. `@trexfeathers`_ added the :class:`~iris.coord_systems.ObliqueMercator` + and :class:`~iris.coord_systems.RotatedMercator` coordinate systems, + complete with NetCDF loading and saving. (:pull:`5548`) + 🐛 Bugs Fixed ============= From 77eba551ded2d88839566ec67d6195fcd6df8aae Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 10:46:47 +0100 Subject: [PATCH 21/33] Temp test disable. --- .github/workflows/ci-manifest.yml | 6 +++--- .github/workflows/ci-tests.yml | 16 ++++++++-------- .github/workflows/ci-wheels.yml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-manifest.yml b/.github/workflows/ci-manifest.yml index 391f944310..f7ce871950 100644 --- a/.github/workflows/ci-manifest.yml +++ b/.github/workflows/ci-manifest.yml @@ -4,9 +4,9 @@ name: ci-manifest on: - pull_request: - branches: - - "*" +# pull_request: +# branches: +# - "*" push: branches-ignore: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 8d84d4e137..58169c1108 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,19 +35,19 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest"] - python-version: ["3.11"] - session: ["doctest", "gallery", "linkcheck"] +# python-version: ["3.11"] +# session: ["doctest", "gallery", "linkcheck"] include: - os: "ubuntu-latest" python-version: "3.11" session: "tests" coverage: "--coverage" - - os: "ubuntu-latest" - python-version: "3.10" - session: "tests" - - os: "ubuntu-latest" - python-version: "3.9" - session: "tests" +# - os: "ubuntu-latest" +# python-version: "3.10" +# session: "tests" +# - os: "ubuntu-latest" +# python-version: "3.9" +# session: "tests" env: IRIS_TEST_DATA_VERSION: "2.21" diff --git a/.github/workflows/ci-wheels.yml b/.github/workflows/ci-wheels.yml index 942d528f6d..ea0b0bb892 100644 --- a/.github/workflows/ci-wheels.yml +++ b/.github/workflows/ci-wheels.yml @@ -9,7 +9,7 @@ name: ci-wheels on: - pull_request: +# pull_request: push: tags: From ff251b75f87e733fe349fce7777a66fa8b637ca4 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 16:48:19 +0100 Subject: [PATCH 22/33] More temp test disabling. --- lib/iris/fileformats/netcdf/saver.py | 2 +- noxfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index fe8aca735b..321cc708c9 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2220,7 +2220,7 @@ def add_ellipsoid(ellipsoid): isinstance(cs, cs_class) for cs_class in ( iris.coord_systems.ObliqueMercator, - iris.coord_systems.RotatedMercator, + iris.coord_systems.ObliqueMercator, ) ], ): diff --git a/noxfile.py b/noxfile.py index 601a1d576e..f0c12303e8 100755 --- a/noxfile.py +++ b/noxfile.py @@ -187,7 +187,7 @@ def tests(session: nox.sessions.Session): "pytest", "-n", "auto", - "lib/iris/tests", + "lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py", ] if "-c" in session.posargs or "--coverage" in session.posargs: run_args[-1:-1] = ["--cov=lib/iris", "--cov-report=xml"] From 8b610e56fa38ec4c05c931570a232026103e1d02 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 16:51:51 +0100 Subject: [PATCH 23/33] WIP testing. --- lib/iris/fileformats/netcdf/saver.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 321cc708c9..7c6a89f9b5 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2215,15 +2215,7 @@ def add_ellipsoid(ellipsoid): # oblique mercator (and rotated variant) # Slightly odd isinstance() check to allow successful mocking. - elif any( - [ - isinstance(cs, cs_class) - for cs_class in ( - iris.coord_systems.ObliqueMercator, - iris.coord_systems.ObliqueMercator, - ) - ], - ): + elif isinstance(cs, iris.coord_systems.RotatedMercator): # RotatedMercator subclasses ObliqueMercator, and RM # instances are implicitly saved as OM due to inherited # properties. This is correct because CF 1.11 is removing From 87a6bcefaf890433447cbaac6fcd7b8966c2a26b Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 16:57:36 +0100 Subject: [PATCH 24/33] WIP testing. --- lib/iris/fileformats/netcdf/saver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 7c6a89f9b5..646709acfe 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2215,7 +2215,9 @@ def add_ellipsoid(ellipsoid): # oblique mercator (and rotated variant) # Slightly odd isinstance() check to allow successful mocking. - elif isinstance(cs, iris.coord_systems.RotatedMercator): + elif isinstance( + cs, iris.coord_systems.ObliqueMercator + ) or isinstance(cs, iris.coord_systems.RotatedMercator): # RotatedMercator subclasses ObliqueMercator, and RM # instances are implicitly saved as OM due to inherited # properties. This is correct because CF 1.11 is removing From 2e700e3caf7b84a1e9aaeecbab08c51bee4c384d Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 17:02:02 +0100 Subject: [PATCH 25/33] Revert "More temp test disabling." This reverts commit ff251b75f87e733fe349fce7777a66fa8b637ca4. --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index f0c12303e8..601a1d576e 100755 --- a/noxfile.py +++ b/noxfile.py @@ -187,7 +187,7 @@ def tests(session: nox.sessions.Session): "pytest", "-n", "auto", - "lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py", + "lib/iris/tests", ] if "-c" in session.posargs or "--coverage" in session.posargs: run_args[-1:-1] = ["--cov=lib/iris", "--cov-report=xml"] From f26bbd7003122d8cf7404aed8f15efebc6fb5ede Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 17:02:26 +0100 Subject: [PATCH 26/33] Revert "Temp test disable." This reverts commit 77eba551ded2d88839566ec67d6195fcd6df8aae. --- .github/workflows/ci-manifest.yml | 6 +++--- .github/workflows/ci-tests.yml | 16 ++++++++-------- .github/workflows/ci-wheels.yml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-manifest.yml b/.github/workflows/ci-manifest.yml index f7ce871950..391f944310 100644 --- a/.github/workflows/ci-manifest.yml +++ b/.github/workflows/ci-manifest.yml @@ -4,9 +4,9 @@ name: ci-manifest on: -# pull_request: -# branches: -# - "*" + pull_request: + branches: + - "*" push: branches-ignore: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 58169c1108..8d84d4e137 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,19 +35,19 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest"] -# python-version: ["3.11"] -# session: ["doctest", "gallery", "linkcheck"] + python-version: ["3.11"] + session: ["doctest", "gallery", "linkcheck"] include: - os: "ubuntu-latest" python-version: "3.11" session: "tests" coverage: "--coverage" -# - os: "ubuntu-latest" -# python-version: "3.10" -# session: "tests" -# - os: "ubuntu-latest" -# python-version: "3.9" -# session: "tests" + - os: "ubuntu-latest" + python-version: "3.10" + session: "tests" + - os: "ubuntu-latest" + python-version: "3.9" + session: "tests" env: IRIS_TEST_DATA_VERSION: "2.21" diff --git a/.github/workflows/ci-wheels.yml b/.github/workflows/ci-wheels.yml index ea0b0bb892..942d528f6d 100644 --- a/.github/workflows/ci-wheels.yml +++ b/.github/workflows/ci-wheels.yml @@ -9,7 +9,7 @@ name: ci-wheels on: -# pull_request: + pull_request: push: tags: From 6a1afae5cd0f8c89f2c34812cdd6e18365bb1f3b Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 23 Oct 2023 17:11:19 +0100 Subject: [PATCH 27/33] Use RotatedMercator inheritance for isinstance() check. --- lib/iris/fileformats/netcdf/saver.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 646709acfe..d8ded259b6 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2214,10 +2214,7 @@ def add_ellipsoid(ellipsoid): cf_var_grid.sweep_angle_axis = cs.sweep_angle_axis # oblique mercator (and rotated variant) - # Slightly odd isinstance() check to allow successful mocking. - elif isinstance( - cs, iris.coord_systems.ObliqueMercator - ) or isinstance(cs, iris.coord_systems.RotatedMercator): + elif isinstance(cs, iris.coord_systems.ObliqueMercator): # RotatedMercator subclasses ObliqueMercator, and RM # instances are implicitly saved as OM due to inherited # properties. This is correct because CF 1.11 is removing From 7a63804a561d304354999b882c00087a527f4c7e Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 24 Oct 2023 08:59:20 +0100 Subject: [PATCH 28/33] Check grid_mapping_name instead of using isinstance(). --- lib/iris/fileformats/netcdf/saver.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index d8ded259b6..b103fca30d 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2214,7 +2214,12 @@ def add_ellipsoid(ellipsoid): cf_var_grid.sweep_angle_axis = cs.sweep_angle_axis # oblique mercator (and rotated variant) - elif isinstance(cs, iris.coord_systems.ObliqueMercator): + # Checking grid_mapping_name instead of using isinstance() + # since subclasses (i.e. RotatedMercator) upset mock tests. + elif ( + getattr(cs, "grid_mapping_name", None) + == "oblique_mercator" + ): # RotatedMercator subclasses ObliqueMercator, and RM # instances are implicitly saved as OM due to inherited # properties. This is correct because CF 1.11 is removing From 55174bf4c4b6af1f6094749229b8a214896b7da3 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 24 Oct 2023 09:15:42 +0100 Subject: [PATCH 29/33] Better type hinting. --- .../test_build_oblique_mercator_coordinate_system.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py index e9699cbf48..59879fc354 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py @@ -7,14 +7,19 @@ Test function :func:`iris.fileformats._nc_load_rules.helpers.build_oblique_mercator_coordinate_system`. """ -from typing import List, NamedTuple +from typing import List, NamedTuple, Type from unittest import mock import pytest from iris import coord_systems from iris._deprecation import IrisDeprecation -from iris.coord_systems import GeogCS, ObliqueMercator, RotatedMercator +from iris.coord_systems import ( + CoordSystem, + GeogCS, + ObliqueMercator, + RotatedMercator, +) from iris.fileformats._nc_load_rules.helpers import ( build_oblique_mercator_coordinate_system, ) @@ -44,7 +49,7 @@ class TestAttributes: class ParamTuple(NamedTuple): id: str nc_attributes: dict - expected_class: [ObliqueMercator, RotatedMercator] + expected_class: Type[CoordSystem] coord_system_kwargs: dict param_list: List[ParamTuple] = [ From 31aa968e9a36223d0de68d62f86c7d5410968896 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 24 Oct 2023 15:09:53 +0100 Subject: [PATCH 30/33] Use return over yield in a fixture. --- lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py b/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py index b25e36d46e..04b9cb8e5c 100644 --- a/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py +++ b/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py @@ -131,7 +131,7 @@ def make_instance(self) -> ObliqueMercator: @pytest.fixture() def instance(self): - yield self.make_instance() + return self.make_instance() def test_instantiate(self): _ = self.make_instance() From 4ddd331af30d0261d66c8fd95a7342ba2b950bfd Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 24 Oct 2023 15:13:23 +0100 Subject: [PATCH 31/33] Duck typing comment. --- lib/iris/fileformats/netcdf/saver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index b103fca30d..8da9bc4436 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2214,8 +2214,8 @@ def add_ellipsoid(ellipsoid): cf_var_grid.sweep_angle_axis = cs.sweep_angle_axis # oblique mercator (and rotated variant) - # Checking grid_mapping_name instead of using isinstance() - # since subclasses (i.e. RotatedMercator) upset mock tests. + # Use duck-typing over isinstance() - subclasses (i.e. + # RotatedMercator) upset mock tests. elif ( getattr(cs, "grid_mapping_name", None) == "oblique_mercator" From 20fca6e70725f4f17897c4d000b990889d5a27b8 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 24 Oct 2023 15:16:50 +0100 Subject: [PATCH 32/33] Better grid_mapping_name checking. --- lib/iris/fileformats/_nc_load_rules/helpers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 374687b29f..9c75c0e866 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -936,8 +936,7 @@ def build_oblique_mercator_coordinate_system(engine, cf_grid_var): oblique_mercator=iris.coord_systems.ObliqueMercator, rotated_mercator=iris.coord_systems.RotatedMercator, ) - SelectedSystem = candidate_systems[grid_mapping_name] - if SelectedSystem is iris.coord_systems.RotatedMercator: + if grid_mapping_name == "rotated_mercator": message = ( "Iris will stop loading the rotated_mercator grid mapping name in " "a future release, in accordance with CF version 1.11 . Instead " @@ -946,7 +945,7 @@ def build_oblique_mercator_coordinate_system(engine, cf_grid_var): warn_deprecated(message) del kwargs[CF_ATTR_GRID_AZIMUTH_CENT_LINE] - cs = SelectedSystem(**kwargs) + cs = candidate_systems[grid_mapping_name](**kwargs) return cs From 2bcee96993cc6f0ed21e2499136b600330177c26 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 24 Oct 2023 15:31:26 +0100 Subject: [PATCH 33/33] Better structure for test parameterisation. --- .../coord_systems/test_ObliqueMercator.py | 170 ++++++++-------- ...uild_oblique_mercator_coordinate_system.py | 191 +++++++++--------- 2 files changed, 187 insertions(+), 174 deletions(-) diff --git a/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py b/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py index 04b9cb8e5c..0799fb881e 100644 --- a/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py +++ b/lib/iris/tests/unit/coord_systems/test_ObliqueMercator.py @@ -19,6 +19,89 @@ #### +class GlobeWithEq(ccrs.Globe): + def __eq__(self, other): + """Need eq to enable comparison with expected arguments.""" + result = NotImplemented + if isinstance(other, ccrs.Globe): + result = other.__dict__ == self.__dict__ + return result + + +class ParamTuple(NamedTuple): + """Used for easy coupling of test parameters.""" + + id: str + class_kwargs: dict + cartopy_kwargs: dict + + +kwarg_permutations: List[ParamTuple] = [ + ParamTuple( + "default", + dict(), + dict(), + ), + ParamTuple( + "azimuth", + dict(azimuth_of_central_line=90), + dict(azimuth=90), + ), + ParamTuple( + "central_longitude", + dict(longitude_of_projection_origin=90), + dict(central_longitude=90), + ), + ParamTuple( + "central_latitude", + dict(latitude_of_projection_origin=45), + dict(central_latitude=45), + ), + ParamTuple( + "false_easting_northing", + dict(false_easting=1000000, false_northing=-2000000), + dict(false_easting=1000000, false_northing=-2000000), + ), + ParamTuple( + "scale_factor", + # Number inherited from Cartopy's test_mercator.py . + dict(scale_factor_at_projection_origin=0.939692620786), + dict(scale_factor=0.939692620786), + ), + ParamTuple( + "globe", + dict(ellipsoid=GeogCS(1)), + dict( + globe=GlobeWithEq(semimajor_axis=1, semiminor_axis=1, ellipse=None) + ), + ), + ParamTuple( + "combo", + dict( + azimuth_of_central_line=90, + longitude_of_projection_origin=90, + latitude_of_projection_origin=45, + false_easting=1000000, + false_northing=-2000000, + scale_factor_at_projection_origin=0.939692620786, + ellipsoid=GeogCS(1), + ), + dict( + azimuth=90.0, + central_longitude=90.0, + central_latitude=45.0, + false_easting=1000000, + false_northing=-2000000, + scale_factor=0.939692620786, + globe=GlobeWithEq( + semimajor_axis=1, semiminor_axis=1, ellipse=None + ), + ), + ), +] +permutation_ids: List[str] = [p.id for p in kwarg_permutations] + + class TestArgs: GeogCS = GeogCS class_kwargs_default = dict( @@ -36,89 +119,12 @@ class TestArgs: globe=None, ) - class GlobeWithEq(ccrs.Globe): - def __eq__(self, other): - """Need eq to enable comparison with expected arguments.""" - result = NotImplemented - if isinstance(other, ccrs.Globe): - result = other.__dict__ == self.__dict__ - return result - - class ParamTuple(NamedTuple): - id: str - class_kwargs: dict - cartopy_kwargs: dict - - param_list: List[ParamTuple] = [ - ParamTuple( - "default", - dict(), - dict(), - ), - ParamTuple( - "azimuth", - dict(azimuth_of_central_line=90), - dict(azimuth=90), - ), - ParamTuple( - "central_longitude", - dict(longitude_of_projection_origin=90), - dict(central_longitude=90), - ), - ParamTuple( - "central_latitude", - dict(latitude_of_projection_origin=45), - dict(central_latitude=45), - ), - ParamTuple( - "false_easting_northing", - dict(false_easting=1000000, false_northing=-2000000), - dict(false_easting=1000000, false_northing=-2000000), - ), - ParamTuple( - "scale_factor", - # Number inherited from Cartopy's test_mercator.py . - dict(scale_factor_at_projection_origin=0.939692620786), - dict(scale_factor=0.939692620786), - ), - ParamTuple( - "globe", - dict(ellipsoid=GeogCS(1)), - dict( - globe=GlobeWithEq( - semimajor_axis=1, semiminor_axis=1, ellipse=None - ) - ), - ), - ParamTuple( - "combo", - dict( - azimuth_of_central_line=90, - longitude_of_projection_origin=90, - latitude_of_projection_origin=45, - false_easting=1000000, - false_northing=-2000000, - scale_factor_at_projection_origin=0.939692620786, - ellipsoid=GeogCS(1), - ), - dict( - azimuth=90.0, - central_longitude=90.0, - central_latitude=45.0, - false_easting=1000000, - false_northing=-2000000, - scale_factor=0.939692620786, - globe=GlobeWithEq( - semimajor_axis=1, semiminor_axis=1, ellipse=None - ), - ), - ), - ] - param_ids: List[str] = [p.id for p in param_list] - - @pytest.fixture(autouse=True, params=param_list, ids=param_ids) + @pytest.fixture( + autouse=True, params=kwarg_permutations, ids=permutation_ids + ) def make_variant_inputs(self, request) -> None: - inputs: TestArgs.ParamTuple = request.param + """Parse a ParamTuple into usable test information.""" + inputs: ParamTuple = request.param self.class_kwargs = dict( self.class_kwargs_default, **inputs.class_kwargs ) diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py index 59879fc354..b11d8d3cca 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_build_oblique_mercator_coordinate_system.py @@ -25,6 +25,100 @@ ) +class ParamTuple(NamedTuple): + """Used for easy coupling of test parameters.""" + + id: str + nc_attributes: dict + expected_class: Type[CoordSystem] + coord_system_kwargs: dict + + +kwarg_permutations: List[ParamTuple] = [ + ParamTuple( + "default", + dict(), + ObliqueMercator, + dict(), + ), + ParamTuple( + "azimuth", + dict(azimuth_of_central_line=90), + ObliqueMercator, + dict(azimuth_of_central_line=90), + ), + ParamTuple( + "central_longitude", + dict(longitude_of_projection_origin=90), + ObliqueMercator, + dict(longitude_of_projection_origin=90), + ), + ParamTuple( + "central_latitude", + dict(latitude_of_projection_origin=45), + ObliqueMercator, + dict(latitude_of_projection_origin=45), + ), + ParamTuple( + "false_easting_northing", + dict(false_easting=1000000, false_northing=-2000000), + ObliqueMercator, + dict(false_easting=1000000, false_northing=-2000000), + ), + ParamTuple( + "scale_factor", + # Number inherited from Cartopy's test_mercator.py . + dict(scale_factor_at_projection_origin=0.939692620786), + ObliqueMercator, + dict(scale_factor_at_projection_origin=0.939692620786), + ), + ParamTuple( + "globe", + dict(semi_major_axis=1), + ObliqueMercator, + dict(ellipsoid=GeogCS(semi_major_axis=1, semi_minor_axis=1)), + ), + ParamTuple( + "combo", + dict( + azimuth_of_central_line=90, + longitude_of_projection_origin=90, + latitude_of_projection_origin=45, + false_easting=1000000, + false_northing=-2000000, + scale_factor_at_projection_origin=0.939692620786, + semi_major_axis=1, + ), + ObliqueMercator, + dict( + azimuth_of_central_line=90.0, + longitude_of_projection_origin=90.0, + latitude_of_projection_origin=45.0, + false_easting=1000000, + false_northing=-2000000, + scale_factor_at_projection_origin=0.939692620786, + ellipsoid=GeogCS(semi_major_axis=1, semi_minor_axis=1), + ), + ), + ParamTuple( + "rotated", + dict(grid_mapping_name="rotated_mercator"), + RotatedMercator, + dict(), + ), + ParamTuple( + "rotated_azimuth_ignored", + dict( + grid_mapping_name="rotated_mercator", + azimuth_of_central_line=45, + ), + RotatedMercator, + dict(), + ), +] +permutation_ids: List[str] = [p.id for p in kwarg_permutations] + + class TestAttributes: """Test that NetCDF attributes are correctly converted to class arguments.""" @@ -46,99 +140,12 @@ class TestAttributes: ellipsoid=None, ) - class ParamTuple(NamedTuple): - id: str - nc_attributes: dict - expected_class: Type[CoordSystem] - coord_system_kwargs: dict - - param_list: List[ParamTuple] = [ - ParamTuple( - "default", - dict(), - ObliqueMercator, - dict(), - ), - ParamTuple( - "azimuth", - dict(azimuth_of_central_line=90), - ObliqueMercator, - dict(azimuth_of_central_line=90), - ), - ParamTuple( - "central_longitude", - dict(longitude_of_projection_origin=90), - ObliqueMercator, - dict(longitude_of_projection_origin=90), - ), - ParamTuple( - "central_latitude", - dict(latitude_of_projection_origin=45), - ObliqueMercator, - dict(latitude_of_projection_origin=45), - ), - ParamTuple( - "false_easting_northing", - dict(false_easting=1000000, false_northing=-2000000), - ObliqueMercator, - dict(false_easting=1000000, false_northing=-2000000), - ), - ParamTuple( - "scale_factor", - # Number inherited from Cartopy's test_mercator.py . - dict(scale_factor_at_projection_origin=0.939692620786), - ObliqueMercator, - dict(scale_factor_at_projection_origin=0.939692620786), - ), - ParamTuple( - "globe", - dict(semi_major_axis=1), - ObliqueMercator, - dict(ellipsoid=GeogCS(semi_major_axis=1, semi_minor_axis=1)), - ), - ParamTuple( - "combo", - dict( - azimuth_of_central_line=90, - longitude_of_projection_origin=90, - latitude_of_projection_origin=45, - false_easting=1000000, - false_northing=-2000000, - scale_factor_at_projection_origin=0.939692620786, - semi_major_axis=1, - ), - ObliqueMercator, - dict( - azimuth_of_central_line=90.0, - longitude_of_projection_origin=90.0, - latitude_of_projection_origin=45.0, - false_easting=1000000, - false_northing=-2000000, - scale_factor_at_projection_origin=0.939692620786, - ellipsoid=GeogCS(semi_major_axis=1, semi_minor_axis=1), - ), - ), - ParamTuple( - "rotated", - dict(grid_mapping_name="rotated_mercator"), - RotatedMercator, - dict(), - ), - ParamTuple( - "rotated_azimuth_ignored", - dict( - grid_mapping_name="rotated_mercator", - azimuth_of_central_line=45, - ), - RotatedMercator, - dict(), - ), - ] - param_ids: List[str] = [p.id for p in param_list] - - @pytest.fixture(autouse=True, params=param_list, ids=param_ids) + @pytest.fixture( + autouse=True, params=kwarg_permutations, ids=permutation_ids + ) def make_variant_inputs(self, request) -> None: - inputs: TestAttributes.ParamTuple = request.param + """Parse a ParamTuple into usable test information.""" + inputs: ParamTuple = request.param self.nc_attributes = dict( self.nc_attributes_default, **inputs.nc_attributes