From 3e9a09482658199092dbc57937d0d7de4dd4dbe5 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 15 Sep 2020 09:50:05 +0100 Subject: [PATCH] Allow passing None for all coord-system optional args. (#3804) * Allow passing None for all coord-system optional args. * Tests for coord-system default args behaviours. * CML fixes for detail changes to coord-systems. * Tests for netcdf load with no false_easting/northing, all relevant systems. * Add whatsnew entry. * Fix matplotlib capitalisation : conflict resolution error. * Handle optional None and parallels in _arg_default. Fix bug in Stereographic.true_scale_lat * Rationalise coord_system optional arg tests. * Fix arg_default parallels. * Fix exception tests for too-many/too-few parallels. * Update lib/iris/tests/unit/coord_systems/test_VerticalPerspective.py Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> * Normalise all coord-system docstrings. Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> --- docs/iris/src/whatsnew/latest.rst | 5 + lib/iris/coord_systems.py | 455 ++++++++++-------- lib/iris/tests/__init__.py | 7 + lib/iris/tests/results/netcdf/netcdf_laea.cml | 8 +- lib/iris/tests/results/netcdf/netcdf_lcc.cml | 8 +- lib/iris/tests/test_coordsystem.py | 4 +- .../coord_systems/test_AlbersEqualArea.py | 57 +++ .../tests/unit/coord_systems/test_GeogCS.py | 51 ++ .../unit/coord_systems/test_Geostationary.py | 45 +- .../test_LambertAzimuthalEqualArea.py | 38 ++ .../coord_systems/test_LambertConformal.py | 78 +++ .../tests/unit/coord_systems/test_Mercator.py | 28 ++ .../unit/coord_systems/test_Orthographic.py | 27 ++ .../unit/coord_systems/test_RotatedPole.py | 17 + .../unit/coord_systems/test_Stereographic.py | 50 ++ .../coord_systems/test_TransverseMercator.py | 58 +++ .../coord_systems/test_VerticalPerspective.py | 26 + ...ild_albers_equal_area_coordinate_system.py | 88 ++++ ..._azimuthal_equal_area_coordinate_system.py | 85 ++++ ...ild_lambert_conformal_coordinate_system.py | 88 ++++ .../test_build_mercator_coordinate_system.py | 14 + ...t_build_stereographic_coordinate_system.py | 69 +-- ...d_transverse_mercator_coordinate_system.py | 82 ++++ .../test_build_verticalp_coordinate_system.py | 29 +- 24 files changed, 1168 insertions(+), 249 deletions(-) create mode 100644 lib/iris/tests/unit/coord_systems/test_GeogCS.py create mode 100644 lib/iris/tests/unit/coord_systems/test_LambertConformal.py create mode 100644 lib/iris/tests/unit/coord_systems/test_Stereographic.py create mode 100644 lib/iris/tests/unit/coord_systems/test_TransverseMercator.py create mode 100644 lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_albers_equal_area_coordinate_system.py create mode 100644 lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_lambert_azimuthal_equal_area_coordinate_system.py create mode 100644 lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_lambert_conformal_coordinate_system.py create mode 100644 lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_transverse_mercator_coordinate_system.py diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 559d2483c1..09bb5207d0 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -92,6 +92,11 @@ Bugs Fixed loading. They are now available on the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. See :pull:`3800`. +* the netcdf loader can now handle any grid-mapping variables with missing + ``false_easting`` and ``false_northing`` properties, which was previously + failing for some coordinate systems. + See :issue:`3629`. + Incompatible Changes ==================== diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index 812dfae23e..82d75ba2b8 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -15,6 +15,35 @@ import cartopy.crs as ccrs +def _arg_default(value, default, cast_as=float): + """Apply a default value and type for an optional kwarg.""" + if value is None: + value = default + value = cast_as(value) + return value + + +def _1or2_parallels(arg): + """Accept 1 or 2 inputs as a tuple of 1 or 2 floats.""" + try: + values_tuple = tuple(arg) + except TypeError: + values_tuple = (arg,) + values_tuple = tuple([float(x) for x in values_tuple]) + nvals = len(values_tuple) + if nvals not in (1, 2): + emsg = "Allows only 1 or 2 parallels or secant latitudes : got {!r}" + raise ValueError(emsg.format(arg)) + return values_tuple + + +def _float_or_None(arg): + """Cast as float, except for allowing None as a distinct valid value.""" + if arg is not None: + arg = float(arg) + return arg + + class CoordSystem(metaclass=ABCMeta): """ Abstract base class for coordinate systems. @@ -107,19 +136,24 @@ def __init__( semi_major_axis=None, semi_minor_axis=None, inverse_flattening=None, - longitude_of_prime_meridian=0, + longitude_of_prime_meridian=None, ): """ Creates a new GeogCS. Kwargs: - * semi_major_axis - of ellipsoid in metres - * semi_minor_axis - of ellipsoid in metres - * inverse_flattening - of ellipsoid - * longitude_of_prime_meridian - Can be used to specify the - prime meridian on the ellipsoid - in degrees. Default = 0. + * semi_major_axis, semi_minor_axis: + Axes of ellipsoid, in metres. At least one must be given + (see note below). + + * inverse_flattening: + Can be omitted if both axes given (see note below). + Defaults to 0.0 . + + * longitude_of_prime_meridian: + Specifies the prime meridian on the ellipsoid, in degrees. + Defaults to 0.0 . If just semi_major_axis is set, with no semi_minor_axis or inverse_flattening, then a perfect sphere is created from the given @@ -204,11 +238,13 @@ def __init__( #: Minor radius of the ellipsoid in metres. self.semi_minor_axis = float(semi_minor_axis) - #: :math:`1/f` where :math:`f = (a-b)/a` + #: :math:`1/f` where :math:`f = (a-b)/a`. self.inverse_flattening = float(inverse_flattening) #: Describes 'zero' on the ellipsoid in degrees. - self.longitude_of_prime_meridian = float(longitude_of_prime_meridian) + self.longitude_of_prime_meridian = _arg_default( + longitude_of_prime_meridian, 0 + ) def _pretty_attrs(self): attrs = [("semi_major_axis", self.semi_major_axis)] @@ -285,7 +321,7 @@ def __init__( self, grid_north_pole_latitude, grid_north_pole_longitude, - north_pole_grid_longitude=0, + north_pole_grid_longitude=None, ellipsoid=None, ): """ @@ -294,17 +330,20 @@ def __init__( Args: - * grid_north_pole_latitude - The true latitude of the rotated - pole in degrees. - * grid_north_pole_longitude - The true longitude of the rotated - pole in degrees. + * grid_north_pole_latitude: + The true latitude of the rotated pole in degrees. + + * grid_north_pole_longitude: + The true longitude of the rotated pole in degrees. Kwargs: - * north_pole_grid_longitude - Longitude of true north pole in - rotated grid in degrees. Default = 0. - * ellipsoid - Optional :class:`GeogCS` defining - the ellipsoid. + * north_pole_grid_longitude: + Longitude of true north pole in rotated grid, in degrees. + Defaults to 0.0 . + + * ellipsoid (:class:`GeogCS`): + If given, defines the ellipsoid. Examples:: @@ -320,9 +359,11 @@ def __init__( self.grid_north_pole_longitude = float(grid_north_pole_longitude) #: Longitude of true north pole in rotated grid in degrees. - self.north_pole_grid_longitude = float(north_pole_grid_longitude) + self.north_pole_grid_longitude = _arg_default( + north_pole_grid_longitude, 0 + ) - #: Ellipsoid definition. + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid def _pretty_attrs(self): @@ -397,9 +438,9 @@ def __init__( self, latitude_of_projection_origin, longitude_of_central_meridian, - false_easting, - false_northing, - scale_factor_at_central_meridian, + false_easting=None, + false_northing=None, + scale_factor_at_central_meridian=None, ellipsoid=None, ): """ @@ -407,27 +448,30 @@ def __init__( Args: - * latitude_of_projection_origin - True latitude of planar origin in degrees. + * latitude_of_projection_origin: + True latitude of planar origin in degrees. - * longitude_of_central_meridian - True longitude of planar origin in degrees. + * longitude_of_central_meridian: + True longitude of planar origin in degrees. - * false_easting - X offset from planar origin in metres. + Kwargs: - * false_northing - Y offset from planar origin in metres. + * false_easting: + X offset from planar origin in metres. + Defaults to 0.0 . - * scale_factor_at_central_meridian - Reduces the cylinder to slice through the ellipsoid - (secant form). Used to provide TWO longitudes of zero - distortion in the area of interest. + * false_northing: + Y offset from planar origin in metres. + Defaults to 0.0 . - Kwargs: + * scale_factor_at_central_meridian: + 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 - Optional :class:`GeogCS` defining the ellipsoid. + * ellipsoid (:class:`GeogCS`): + If given, defines the ellipsoid. Example:: @@ -447,17 +491,17 @@ def __init__( ) #: X offset from planar origin in metres. - self.false_easting = float(false_easting) + self.false_easting = _arg_default(false_easting, 0) #: Y offset from planar origin in metres. - self.false_northing = float(false_northing) + self.false_northing = _arg_default(false_northing, 0) - #: Reduces the cylinder to slice through the ellipsoid (secant form). - self.scale_factor_at_central_meridian = float( - scale_factor_at_central_meridian + #: Scale factor at the centre longitude. + self.scale_factor_at_central_meridian = _arg_default( + scale_factor_at_central_meridian, 1.0 ) - #: Ellipsoid definition. + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid def __repr__(self): @@ -524,8 +568,8 @@ def __init__( self, latitude_of_projection_origin, longitude_of_projection_origin, - false_easting=0.0, - false_northing=0.0, + false_easting=None, + false_northing=None, ellipsoid=None, ): """ @@ -541,14 +585,14 @@ def __init__( Kwargs: - * false_easting - X offset from planar origin in metres. Defaults to 0. + * false_easting: + X offset from planar origin in metres. Defaults to 0.0 . - * false_northing - Y offset from planar origin in metres. Defaults to 0. + * false_northing: + Y offset from planar origin in metres. Defaults to 0.0 . - * ellipsoid - :class:`GeogCS` defining the ellipsoid. + * ellipsoid (:class:`GeogCS`): + If given, defines the ellipsoid. """ #: True latitude of planar origin in degrees. @@ -562,12 +606,12 @@ def __init__( ) #: X offset from planar origin in metres. - self.false_easting = float(false_easting) + self.false_easting = _arg_default(false_easting, 0) #: Y offset from planar origin in metres. - self.false_northing = float(false_northing) + self.false_northing = _arg_default(false_northing, 0) - #: Ellipsoid definition. + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid def __repr__(self): @@ -615,8 +659,8 @@ def __init__( latitude_of_projection_origin, longitude_of_projection_origin, perspective_point_height, - false_easting=0, - false_northing=0, + false_easting=None, + false_northing=None, ellipsoid=None, ): """ @@ -636,14 +680,14 @@ def __init__( Kwargs: - * false_easting - X offset from planar origin in metres. Defaults to 0. + * false_easting: + X offset from planar origin in metres. Defaults to 0.0 . - * false_northing - Y offset from planar origin in metres. Defaults to 0. + * false_northing: + Y offset from planar origin in metres. Defaults to 0.0 . - * ellipsoid - :class:`GeogCS` defining the ellipsoid. + * ellipsoid (:class:`GeogCS`): + If given, defines the ellipsoid. """ #: True latitude of planar origin in degrees. @@ -657,16 +701,16 @@ def __init__( ) #: Altitude of satellite in metres. - # test if perspective_point_height may be cast to float for proj.4 self.perspective_point_height = float(perspective_point_height) + # TODO: test if may be cast to float for proj.4 #: X offset from planar origin in metres. - self.false_easting = float(false_easting) + self.false_easting = _arg_default(false_easting, 0) #: Y offset from planar origin in metres. - self.false_northing = float(false_northing) + self.false_northing = _arg_default(false_northing, 0) - #: Ellipsoid definition. + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid def __repr__(self): @@ -725,13 +769,13 @@ def __init__( Args: - * latitude_of_projection_origin (float): + * latitude_of_projection_origin: True latitude of planar origin in degrees. - * longitude_of_projection_origin (float): + * longitude_of_projection_origin: True longitude of planar origin in degrees. - * perspective_point_height (float): + * perspective_point_height: Altitude of satellite in metres above the surface of the ellipsoid. * sweep_angle_axis (string): @@ -739,14 +783,14 @@ def __init__( Kwargs: - * false_easting (float): - X offset from planar origin in metres. Defaults to 0. + * false_easting: + X offset from planar origin in metres. Defaults to 0.0 . - * false_northing (float): - Y offset from planar origin in metres. Defaults to 0. + * false_northing: + Y offset from planar origin in metres. Defaults to 0.0 . - * ellipsoid (iris.coord_systems.GeogCS): - :class:`GeogCS` defining the ellipsoid. + * ellipsoid (:class:`GeogCS`): + If given, defines the ellipsoid. """ #: True latitude of planar origin in degrees. @@ -765,25 +809,21 @@ def __init__( ) #: Altitude of satellite in metres. - # test if perspective_point_height may be cast to float for proj.4 self.perspective_point_height = float(perspective_point_height) + # TODO: test if may be cast to float for proj.4 #: X offset from planar origin in metres. - if false_easting is None: - false_easting = 0 - self.false_easting = float(false_easting) + self.false_easting = _arg_default(false_easting, 0) #: Y offset from planar origin in metres. - if false_northing is None: - false_northing = 0 - self.false_northing = float(false_northing) + self.false_northing = _arg_default(false_northing, 0) - #: The axis along which the satellite instrument sweeps - 'x' or 'y'. + #: The sweep angle axis (string 'x' or 'y'). self.sweep_angle_axis = sweep_angle_axis if self.sweep_angle_axis not in ("x", "y"): raise ValueError('Invalid sweep_angle_axis - must be "x" or "y"') - #: Ellipsoid definition. + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid def __repr__(self): @@ -831,8 +871,8 @@ def __init__( self, central_lat, central_lon, - false_easting=0.0, - false_northing=0.0, + false_easting=None, + false_northing=None, true_scale_lat=None, ellipsoid=None, ): @@ -841,25 +881,25 @@ def __init__( Args: - * central_lat - The latitude of the pole. + * central_lat: + The latitude of the pole. - * central_lon - The central longitude, which aligns with the y axis. + * central_lon: + The central longitude, which aligns with the y axis. Kwargs: - * false_easting - X offset from planar origin in metres. Defaults to 0. + * false_easting: + X offset from planar origin in metres. Defaults to 0.0 . - * false_northing - Y offset from planar origin in metres. Defaults to 0. + * false_northing: + Y offset from planar origin in metres. Defaults to 0.0 . - * true_scale_lat - Latitude of true scale. + * true_scale_lat: + Latitude of true scale. - * ellipsoid - :class:`GeogCS` defining the ellipsoid. + * ellipsoid (:class:`GeogCS`): + If given, defines the ellipsoid. """ @@ -870,15 +910,19 @@ def __init__( self.central_lon = float(central_lon) #: X offset from planar origin in metres. - self.false_easting = float(false_easting) + self.false_easting = _arg_default(false_easting, 0) #: Y offset from planar origin in metres. - self.false_northing = float(false_northing) + self.false_northing = _arg_default(false_northing, 0) #: Latitude of true scale. - self.true_scale_lat = float(true_scale_lat) if true_scale_lat else None + 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, + # and *not* to 0.0 . - #: Ellipsoid definition. + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid def __repr__(self): @@ -922,11 +966,11 @@ class LambertConformal(CoordSystem): def __init__( self, - central_lat=39.0, - central_lon=-96.0, - false_easting=0.0, - false_northing=0.0, - secant_latitudes=(33, 45), + central_lat=None, + central_lon=None, + false_easting=None, + false_northing=None, + secant_latitudes=None, ellipsoid=None, ): """ @@ -934,23 +978,24 @@ def __init__( Kwargs: - * central_lat - The latitude of "unitary scale". + * central_lat: + The latitude of "unitary scale". Defaults to 39.0 . - * central_lon - The central longitude. + * central_lon: + The central longitude. Defaults to -96.0 . - * false_easting - X offset from planar origin in metres. + * false_easting: + X offset from planar origin in metres. Defaults to 0.0 . - * false_northing - Y offset from planar origin in metres. + * false_northing: + Y offset from planar origin in metres. Defaults to 0.0 . - * secant_latitudes - Latitudes of secant intersection. + * secant_latitudes (number or iterable of 1 or 2 numbers): + Latitudes of secant intersection. One or two. + Defaults to (33.0, 45.0). - * ellipsoid - :class:`GeogCS` defining the ellipsoid. + * ellipsoid (:class:`GeogCS`): + If given, defines the ellipsoid. .. note: @@ -962,23 +1007,23 @@ def __init__( """ #: True latitude of planar origin in degrees. - self.central_lat = central_lat + self.central_lat = _arg_default(central_lat, 39.0) + #: True longitude of planar origin in degrees. - self.central_lon = central_lon + self.central_lon = _arg_default(central_lon, -96.0) + #: X offset from planar origin in metres. - self.false_easting = false_easting + self.false_easting = _arg_default(false_easting, 0) + #: Y offset from planar origin in metres. - self.false_northing = false_northing - #: The one or two standard parallels of the cone. - try: - self.secant_latitudes = tuple(secant_latitudes) - except TypeError: - self.secant_latitudes = (secant_latitudes,) - nlats = len(self.secant_latitudes) - if nlats == 0 or nlats > 2: - emsg = "Either one or two secant latitudes required, got {}" - raise ValueError(emsg.format(nlats)) - #: Ellipsoid definition. + self.false_northing = _arg_default(false_northing, 0) + + #: The standard parallels of the cone (tuple of 1 or 2 floats). + self.secant_latitudes = _arg_default( + secant_latitudes, (33, 45), cast_as=_1or2_parallels + ) + + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid def __repr__(self): @@ -1032,28 +1077,35 @@ class Mercator(CoordSystem): def __init__( self, - longitude_of_projection_origin=0.0, + longitude_of_projection_origin=None, ellipsoid=None, - standard_parallel=0.0, + standard_parallel=None, ): """ Constructs a Mercator coord system. Kwargs: - * longitude_of_projection_origin - True longitude of planar origin in degrees. - * ellipsoid - :class:`GeogCS` defining the ellipsoid. - * standard_parallel - the latitude where the scale is 1. Defaults to 0 degrees. + + * longitude_of_projection_origin: + True longitude of planar origin in degrees. Defaults to 0.0 . + + * ellipsoid (:class:`GeogCS`): + If given, defines the ellipsoid. + + * standard_parallel: + The latitude where the scale is 1. Defaults to 0.0 . """ #: True longitude of planar origin in degrees. - self.longitude_of_projection_origin = longitude_of_projection_origin - #: Ellipsoid definition. + self.longitude_of_projection_origin = _arg_default( + longitude_of_projection_origin, 0 + ) + + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid - #: The latitude where the scale is 1 (defaults to 0 degrees). - self.standard_parallel = standard_parallel + + #: The latitude where the scale is 1. + self.standard_parallel = _arg_default(standard_parallel, 0) def __repr__(self): res = ( @@ -1087,10 +1139,10 @@ class LambertAzimuthalEqualArea(CoordSystem): def __init__( self, - latitude_of_projection_origin=0.0, - longitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, + latitude_of_projection_origin=None, + longitude_of_projection_origin=None, + false_easting=None, + false_northing=None, ellipsoid=None, ): """ @@ -1098,31 +1150,39 @@ def __init__( Kwargs: - * latitude_of_projection_origin - True latitude of planar origin in degrees. Defaults to 0. + * latitude_of_projection_origin: + True latitude of planar origin in degrees. Defaults to 0.0 . - * longitude_of_projection_origin - True longitude of planar origin in degrees. Defaults to 0. + * longitude_of_projection_origin: + True longitude of planar origin in degrees. Defaults to 0.0 . - * false_easting - X offset from planar origin in metres. Defaults to 0. + * false_easting: + X offset from planar origin in metres. Defaults to 0.0 . - * false_northing - Y offset from planar origin in metres. Defaults to 0. + * false_northing: + Y offset from planar origin in metres. Defaults to 0.0 . - * ellipsoid - :class:`GeogCS` defining the ellipsoid. + * ellipsoid (:class:`GeogCS`): + If given, defines the ellipsoid. """ #: True latitude of planar origin in degrees. - self.latitude_of_projection_origin = latitude_of_projection_origin + self.latitude_of_projection_origin = _arg_default( + latitude_of_projection_origin, 0 + ) + #: True longitude of planar origin in degrees. - self.longitude_of_projection_origin = longitude_of_projection_origin + self.longitude_of_projection_origin = _arg_default( + longitude_of_projection_origin, 0 + ) + #: X offset from planar origin in metres. - self.false_easting = false_easting + self.false_easting = _arg_default(false_easting, 0) + #: Y offset from planar origin in metres. - self.false_northing = false_northing - #: Ellipsoid definition. + self.false_northing = _arg_default(false_northing, 0) + + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid def __repr__(self): @@ -1163,11 +1223,11 @@ class AlbersEqualArea(CoordSystem): def __init__( self, - latitude_of_projection_origin=0.0, - longitude_of_central_meridian=0.0, - false_easting=0.0, - false_northing=0.0, - standard_parallels=(20.0, 50.0), + latitude_of_projection_origin=None, + longitude_of_central_meridian=None, + false_easting=None, + false_northing=None, + standard_parallels=None, ellipsoid=None, ): """ @@ -1175,38 +1235,49 @@ def __init__( Kwargs: - * latitude_of_projection_origin - True latitude of planar origin in degrees. - Defaults to 0. + * latitude_of_projection_origin: + True latitude of planar origin in degrees. Defaults to 0.0 . + + * longitude_of_central_meridian: + True longitude of planar central meridian in degrees. + Defaults to 0.0 . - * longitude_of_central_meridian - True longitude of planar central meridian in degrees. - Defaults to 0. + * false_easting: + X offset from planar origin in metres. Defaults to 0.0 . - * false_easting - X offset from planar origin in metres. Defaults to 0. + * false_northing: + Y offset from planar origin in metres. Defaults to 0.0 . - * false_northing - Y offset from planar origin in metres. Defaults to 0. + * standard_parallels (number or iterable of 1 or 2 numbers): + The one or two latitudes of correct scale. + Defaults to (20.0, 50.0). - * standard_parallels - The one or two latitudes of correct scale. - Defaults to (20,50). - * ellipsoid - :class:`GeogCS` defining the ellipsoid. + * ellipsoid (:class:`GeogCS`): + If given, defines the ellipsoid. """ #: True latitude of planar origin in degrees. - self.latitude_of_projection_origin = latitude_of_projection_origin + self.latitude_of_projection_origin = _arg_default( + latitude_of_projection_origin, 0 + ) + #: True longitude of planar central meridian in degrees. - self.longitude_of_central_meridian = longitude_of_central_meridian + self.longitude_of_central_meridian = _arg_default( + longitude_of_central_meridian, 0 + ) + #: X offset from planar origin in metres. - self.false_easting = false_easting + self.false_easting = _arg_default(false_easting, 0) + #: Y offset from planar origin in metres. - self.false_northing = false_northing - #: The one or two latitudes of correct scale. - self.standard_parallels = standard_parallels - #: Ellipsoid definition. + self.false_northing = _arg_default(false_northing, 0) + + #: The one or two latitudes of correct scale (tuple of 1 or 2 floats). + self.standard_parallels = _arg_default( + standard_parallels, (20, 50), cast_as=_1or2_parallels + ) + + #: Ellipsoid definition (:class:`GeogCS` or None). self.ellipsoid = ellipsoid def __repr__(self): diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py index b5b80a97ef..2d97c62506 100644 --- a/lib/iris/tests/__init__.py +++ b/lib/iris/tests/__init__.py @@ -1077,6 +1077,13 @@ def assertDictEqual(self, lhs, rhs, msg=None): ) raise AssertionError(emsg) + def assertEqualAndKind(self, value, expected): + # Check a value, and also its type 'kind' = float/integer/string. + self.assertEqual(value, expected) + self.assertEqual( + np.array(value).dtype.kind, np.array(expected).dtype.kind + ) + # An environment variable controls whether test timings are output. # diff --git a/lib/iris/tests/results/netcdf/netcdf_laea.cml b/lib/iris/tests/results/netcdf/netcdf_laea.cml index 03fdb529a4..ad23114038 100644 --- a/lib/iris/tests/results/netcdf/netcdf_laea.cml +++ b/lib/iris/tests/results/netcdf/netcdf_laea.cml @@ -31,12 +31,12 @@ [5262500.0, 5701785.71429], [5701785.71429, 6141071.42857], [6141071.42857, 6580357.14286], - [6580357.14286, 7019642.85714]]" id="950c6ce8" points="[650000.0, 1089285.71429, 1528571.42857, + [6580357.14286, 7019642.85714]]" id="b71cdf0e" points="[650000.0, 1089285.71429, 1528571.42857, 1967857.14286, 2407142.85714, 2846428.57143, 3285714.28571, 3725000.0, 4164285.71429, 4603571.42857, 5042857.14286, 5482142.85714, 5921428.57143, 6360714.28571, 6800000.0]" shape="(15,)" standard_name="projection_x_coordinate" units="Unit('m')" value_type="float64" var_name="projection_x_coordinate"> - + @@ -54,12 +54,12 @@ [4500000.0, 4871428.57143], [4871428.57143, 5242857.14286], [5242857.14286, 5614285.71429], - [5614285.71429, 5985714.28571]]" id="fbb8fa7a" points="[600000.0, 971428.571429, 1342857.14286, + [5614285.71429, 5985714.28571]]" id="f1f8b7cb" points="[600000.0, 971428.571429, 1342857.14286, 1714285.71429, 2085714.28571, 2457142.85714, 2828571.42857, 3200000.0, 3571428.57143, 3942857.14286, 4314285.71429, 4685714.28571, 5057142.85714, 5428571.42857, 5800000.0]" shape="(15,)" standard_name="projection_y_coordinate" units="Unit('m')" value_type="float64" var_name="projection_y_coordinate"> - + diff --git a/lib/iris/tests/results/netcdf/netcdf_lcc.cml b/lib/iris/tests/results/netcdf/netcdf_lcc.cml index ace6c632d9..7ea53e6600 100644 --- a/lib/iris/tests/results/netcdf/netcdf_lcc.cml +++ b/lib/iris/tests/results/netcdf/netcdf_lcc.cml @@ -48,7 +48,7 @@ ..., 11.7454500198, 11.7587404251, 11.77202034]]" shape="(60, 60)" standard_name="longitude" units="Unit('degrees')" value_type="float64" var_name="lon"/> - - + - - + diff --git a/lib/iris/tests/test_coordsystem.py b/lib/iris/tests/test_coordsystem.py index 3b89215dba..5babf1a9e2 100644 --- a/lib/iris/tests/test_coordsystem.py +++ b/lib/iris/tests/test_coordsystem.py @@ -432,12 +432,12 @@ def test_as_cartopy_projection(self): class Test_LambertConformal(tests.GraphicsTest): def test_fail_secant_latitudes_none(self): - emsg = "one or two secant latitudes required" + emsg = "secant latitudes" with self.assertRaisesRegex(ValueError, emsg): LambertConformal(secant_latitudes=()) def test_fail_secant_latitudes_excessive(self): - emsg = "one or two secant latitudes required" + emsg = "secant latitudes" with self.assertRaisesRegex(ValueError, emsg): LambertConformal(secant_latitudes=(1, 2, 3)) diff --git a/lib/iris/tests/unit/coord_systems/test_AlbersEqualArea.py b/lib/iris/tests/unit/coord_systems/test_AlbersEqualArea.py index 36e38e90d4..09e69feff9 100644 --- a/lib/iris/tests/unit/coord_systems/test_AlbersEqualArea.py +++ b/lib/iris/tests/unit/coord_systems/test_AlbersEqualArea.py @@ -52,6 +52,16 @@ def test_crs_creation(self): ) self.assertEqual(res, expected) + def test_fail_too_few_parallels(self): + emsg = "parallels" + with self.assertRaisesRegex(ValueError, emsg): + AlbersEqualArea(standard_parallels=()) + + def test_fail_too_many_parallels(self): + emsg = "parallels" + with self.assertRaisesRegex(ValueError, emsg): + AlbersEqualArea(standard_parallels=(1, 2, 3)) + class Test_as_cartopy_projection(tests.IrisTest): def setUp(self): @@ -90,5 +100,52 @@ def test_projection_creation(self): self.assertEqual(res, expected) +class Test_init_defaults(tests.IrisTest): + def test_set_optional_args(self): + # Check that setting optional arguments works as expected. + crs = AlbersEqualArea( + longitude_of_central_meridian=123, + latitude_of_projection_origin=-17, + false_easting=100, + false_northing=-200, + standard_parallels=(-37, 21.4), + ) + + self.assertEqualAndKind(crs.longitude_of_central_meridian, 123.0) + self.assertEqualAndKind(crs.latitude_of_projection_origin, -17.0) + self.assertEqualAndKind(crs.false_easting, 100.0) + self.assertEqualAndKind(crs.false_northing, -200.0) + self.assertEqual(len(crs.standard_parallels), 2) + self.assertEqualAndKind(crs.standard_parallels[0], -37.0) + self.assertEqualAndKind(crs.standard_parallels[1], 21.4) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.longitude_of_central_meridian, 0.0) + self.assertEqualAndKind(crs.latitude_of_projection_origin, 0.0) + self.assertEqualAndKind(crs.false_easting, 0.0) + self.assertEqualAndKind(crs.false_northing, 0.0) + self.assertEqual(len(crs.standard_parallels), 2) + self.assertEqualAndKind(crs.standard_parallels[0], 20.0) + self.assertEqualAndKind(crs.standard_parallels[1], 50.0) + + def test_no_optional_args(self): + # Check expected defaults with no optional args. + crs = AlbersEqualArea() + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected defaults with optional args=None. + crs = AlbersEqualArea( + longitude_of_central_meridian=None, + latitude_of_projection_origin=None, + standard_parallels=None, + false_easting=None, + false_northing=None, + ) + self._check_crs_defaults(crs) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/coord_systems/test_GeogCS.py b/lib/iris/tests/unit/coord_systems/test_GeogCS.py new file mode 100644 index 0000000000..c2882272a3 --- /dev/null +++ b/lib/iris/tests/unit/coord_systems/test_GeogCS.py @@ -0,0 +1,51 @@ +# 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.GeogCS` class.""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from iris.coord_systems import GeogCS + + +class Test_init_defaults(tests.IrisTest): + # NOTE: most of the testing for GeogCS is in the legacy test module + # 'iris.tests.test_coordsystem'. + # This class *only* tests the defaults for optional constructor args. + + def test_set_optional_args(self): + # Check that setting the optional (non-ellipse) argument works. + crs = GeogCS(1.0, longitude_of_prime_meridian=-85) + self.assertEqualAndKind(crs.longitude_of_prime_meridian, -85.0) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + radius = float(crs.semi_major_axis) + self.assertEqualAndKind(crs.semi_major_axis, radius) # just the kind + self.assertEqualAndKind(crs.semi_minor_axis, radius) + self.assertEqualAndKind(crs.inverse_flattening, 0.0) + self.assertEqualAndKind(crs.longitude_of_prime_meridian, 0.0) + + def test_no_optional_args(self): + # Check expected properties with no optional args. + crs = GeogCS(1.0) + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected properties with optional args=None. + crs = GeogCS( + 1.0, + semi_minor_axis=None, + inverse_flattening=None, + longitude_of_prime_meridian=None, + ) + self._check_crs_defaults(crs) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/coord_systems/test_Geostationary.py b/lib/iris/tests/unit/coord_systems/test_Geostationary.py index 2c08a13872..d1418cc6de 100644 --- a/lib/iris/tests/unit/coord_systems/test_Geostationary.py +++ b/lib/iris/tests/unit/coord_systems/test_Geostationary.py @@ -15,15 +15,16 @@ class Test(tests.IrisTest): def setUp(self): - self.latitude_of_projection_origin = 0.0 - self.longitude_of_projection_origin = 0.0 - self.perspective_point_height = 35785831.0 - self.sweep_angle_axis = "y" - self.false_easting = 0.0 - self.false_northing = 0.0 - - self.semi_major_axis = 6377563.396 - self.semi_minor_axis = 6356256.909 + # Set everything to non-default values. + self.latitude_of_projection_origin = 0 # For now, Cartopy needs =0. + self.longitude_of_projection_origin = 123.0 + self.perspective_point_height = 9999.0 + self.sweep_angle_axis = "x" + self.false_easting = 100.0 + self.false_northing = -200.0 + + self.semi_major_axis = 4000.0 + self.semi_minor_axis = 3900.0 self.ellipsoid = GeogCS(self.semi_major_axis, self.semi_minor_axis) self.globe = ccrs.Globe( semimajor_axis=self.semi_major_axis, @@ -83,6 +84,32 @@ def test_invalid_sweep(self): self.ellipsoid, ) + def test_set_optional_args(self): + # Check that setting the optional (non-ellipse) args works. + crs = Geostationary( + 0, 0, 1000, "y", false_easting=100, false_northing=-200 + ) + self.assertEqualAndKind(crs.false_easting, 100.0) + self.assertEqualAndKind(crs.false_northing, -200.0) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.false_easting, 0.0) + self.assertEqualAndKind(crs.false_northing, 0.0) + + def test_no_optional_args(self): + # Check expected defaults with no optional args. + crs = Geostationary(0, 0, 1000, "y") + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected defaults with optional args=None. + crs = Geostationary( + 0, 0, 1000, "y", false_easting=None, false_northing=None + ) + self._check_crs_defaults(crs) + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/coord_systems/test_LambertAzimuthalEqualArea.py b/lib/iris/tests/unit/coord_systems/test_LambertAzimuthalEqualArea.py index c4a283c3b9..fd0d5e8f60 100644 --- a/lib/iris/tests/unit/coord_systems/test_LambertAzimuthalEqualArea.py +++ b/lib/iris/tests/unit/coord_systems/test_LambertAzimuthalEqualArea.py @@ -84,5 +84,43 @@ def test_projection_creation(self): self.assertEqual(res, expected) +class Test_init_defaults(tests.IrisTest): + def test_set_optional_args(self): + # Check that setting the optional (non-ellipse) args works. + crs = LambertAzimuthalEqualArea( + longitude_of_projection_origin=123, + latitude_of_projection_origin=-37, + false_easting=100, + false_northing=-200, + ) + self.assertEqualAndKind(crs.longitude_of_projection_origin, 123.0) + self.assertEqualAndKind(crs.latitude_of_projection_origin, -37.0) + self.assertEqualAndKind(crs.false_easting, 100.0) + self.assertEqualAndKind(crs.false_northing, -200.0) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.longitude_of_projection_origin, 0.0) + self.assertEqualAndKind(crs.latitude_of_projection_origin, 0.0) + self.assertEqualAndKind(crs.false_easting, 0.0) + self.assertEqualAndKind(crs.false_northing, 0.0) + + def test_no_optional_args(self): + # Check expected defaults with no optional args. + crs = LambertAzimuthalEqualArea() + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected defaults with optional args=None. + crs = LambertAzimuthalEqualArea( + longitude_of_projection_origin=None, + latitude_of_projection_origin=None, + false_easting=None, + false_northing=None, + ) + self._check_crs_defaults(crs) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/coord_systems/test_LambertConformal.py b/lib/iris/tests/unit/coord_systems/test_LambertConformal.py new file mode 100644 index 0000000000..da360fa81c --- /dev/null +++ b/lib/iris/tests/unit/coord_systems/test_LambertConformal.py @@ -0,0 +1,78 @@ +# 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.LambertConformal` class.""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from iris.coord_systems import LambertConformal + + +class Test_init_defaults(tests.IrisTest): + # NOTE: most of the testing for LambertConformal is in the legacy test + # module 'iris.tests.test_coordsystem'. + # This class *only* tests the defaults for optional constructor args. + + def test_set_optional_args(self): + # Check that setting the optional (non-ellipse) args works. + # (Except secant_latitudes, which are done separately). + crs = LambertConformal( + central_lat=25.3, + central_lon=-172, + false_easting=100, + false_northing=-200, + ) + self.assertEqualAndKind(crs.central_lat, 25.3) + self.assertEqualAndKind(crs.central_lon, -172.0) + self.assertEqualAndKind(crs.false_easting, 100.0) + self.assertEqualAndKind(crs.false_northing, -200.0) + + def test_set_one_parallel(self): + # Check that setting the optional (non-ellipse) args works. + # (Except secant_latitudes, which are done separately). + crs = LambertConformal(secant_latitudes=-44) + self.assertEqual(len(crs.secant_latitudes), 1) + self.assertEqualAndKind(crs.secant_latitudes[0], -44.0) + + def test_set_two_parallels(self): + # Check that setting the optional (non-ellipse) args works. + # (Except secant_latitudes, which are done separately). + crs = LambertConformal(secant_latitudes=[43, -7]) + self.assertEqual(len(crs.secant_latitudes), 2) + self.assertEqualAndKind(crs.secant_latitudes[0], 43.0) + self.assertEqualAndKind(crs.secant_latitudes[1], -7.0) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.central_lat, 39.0) + self.assertEqualAndKind(crs.central_lon, -96.0) + self.assertEqualAndKind(crs.false_easting, 0.0) + self.assertEqualAndKind(crs.false_northing, 0.0) + self.assertEqual(len(crs.secant_latitudes), 2) + self.assertEqualAndKind(crs.secant_latitudes[0], 33.0) + self.assertEqualAndKind(crs.secant_latitudes[1], 45.0) + + def test_no_optional_args(self): + # Check expected defaults with no optional args. + crs = LambertConformal() + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected defaults with optional args=None. + crs = LambertConformal( + central_lat=None, + central_lon=None, + false_easting=None, + false_northing=None, + secant_latitudes=None, + ) + self._check_crs_defaults(crs) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/coord_systems/test_Mercator.py b/lib/iris/tests/unit/coord_systems/test_Mercator.py index 65668c895c..d48f81f61e 100644 --- a/lib/iris/tests/unit/coord_systems/test_Mercator.py +++ b/lib/iris/tests/unit/coord_systems/test_Mercator.py @@ -33,6 +33,34 @@ def test_repr(self): self.assertEqual(expected, repr(self.tm)) +class Test_init_defaults(tests.IrisTest): + def test_set_optional_args(self): + # Check that setting the optional (non-ellipse) args works. + crs = Mercator( + longitude_of_projection_origin=27, standard_parallel=157.4 + ) + self.assertEqualAndKind(crs.longitude_of_projection_origin, 27.0) + self.assertEqualAndKind(crs.standard_parallel, 157.4) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.longitude_of_projection_origin, 0.0) + self.assertEqualAndKind(crs.standard_parallel, 0.0) + + def test_no_optional_args(self): + # Check expected defaults with no optional args. + crs = Mercator() + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected defaults with optional args=None. + crs = Mercator( + longitude_of_projection_origin=None, standard_parallel=None + ) + self._check_crs_defaults(crs) + + class Test_Mercator__as_cartopy_crs(tests.IrisTest): def test_simple(self): # Check that a projection set up with all the defaults is correctly diff --git a/lib/iris/tests/unit/coord_systems/test_Orthographic.py b/lib/iris/tests/unit/coord_systems/test_Orthographic.py index ae00f4e8c7..a7c5d49736 100644 --- a/lib/iris/tests/unit/coord_systems/test_Orthographic.py +++ b/lib/iris/tests/unit/coord_systems/test_Orthographic.py @@ -69,5 +69,32 @@ def test_projection_creation(self): self.assertEqual(res, expected) +class Test_init_defaults(tests.IrisTest): + # NOTE: most of the testing for Orthographic.__init__ is elsewhere. + # This class *only* tests the defaults for optional constructor args. + + def test_set_optional_args(self): + # Check that setting the optional (non-ellipse) args works. + crs = Orthographic(0, 0, false_easting=100, false_northing=-203.7) + self.assertEqualAndKind(crs.false_easting, 100.0) + self.assertEqualAndKind(crs.false_northing, -203.7) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.false_easting, 0.0) + self.assertEqualAndKind(crs.false_northing, 0.0) + + def test_no_optional_args(self): + # Check expected defaults with no optional args. + crs = Orthographic(0, 0) + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected defaults with optional args=None. + crs = Orthographic(0, 0, false_easting=None, false_northing=None) + self._check_crs_defaults(crs) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/coord_systems/test_RotatedPole.py b/lib/iris/tests/unit/coord_systems/test_RotatedPole.py index 3d99dbc13f..52fdaf02bd 100644 --- a/lib/iris/tests/unit/coord_systems/test_RotatedPole.py +++ b/lib/iris/tests/unit/coord_systems/test_RotatedPole.py @@ -62,6 +62,23 @@ def test_as_cartopy_projection(self): sorted(expected.proj4_init.split(" +")), ) + def _check_crs_default(self, crs): + # Check for property defaults when no kwargs options are set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.north_pole_grid_longitude, 0.0) + + def test_optional_args_missing(self): + # Check that unused 'north_pole_grid_longitude' defaults to 0.0. + crs = RotatedGeogCS(self.pole_lon, self.pole_lat) + self._check_crs_default(crs) + + def test_optional_args_None(self): + # Check that 'north_pole_grid_longitude=None' defaults to 0.0. + crs = RotatedGeogCS( + self.pole_lon, self.pole_lat, north_pole_grid_longitude=None + ) + self._check_crs_default(crs) + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/coord_systems/test_Stereographic.py b/lib/iris/tests/unit/coord_systems/test_Stereographic.py new file mode 100644 index 0000000000..d505906e22 --- /dev/null +++ b/lib/iris/tests/unit/coord_systems/test_Stereographic.py @@ -0,0 +1,50 @@ +# 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.Stereographic` class.""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from iris.coord_systems import Stereographic + + +class Test_init_defaults(tests.IrisTest): + # NOTE: most of the testing for Stereographic is in the legacy test module + # 'iris.tests.test_coordsystem'. + # This class *only* tests the defaults for optional constructor args. + + def test_set_optional_args(self): + # Check that setting the optional (non-ellipse) args works. + crs = Stereographic( + 0, 0, false_easting=100, false_northing=-203.7, true_scale_lat=77 + ) + self.assertEqualAndKind(crs.false_easting, 100.0) + self.assertEqualAndKind(crs.false_northing, -203.7) + self.assertEqualAndKind(crs.true_scale_lat, 77.0) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.false_easting, 0.0) + self.assertEqualAndKind(crs.false_northing, 0.0) + self.assertIsNone(crs.true_scale_lat) + + def test_no_optional_args(self): + # Check expected defaults with no optional args. + crs = Stereographic(0, 0) + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected defaults with optional args=None. + crs = Stereographic( + 0, 0, false_easting=None, false_northing=None, true_scale_lat=None + ) + self._check_crs_defaults(crs) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/coord_systems/test_TransverseMercator.py b/lib/iris/tests/unit/coord_systems/test_TransverseMercator.py new file mode 100644 index 0000000000..1d04463316 --- /dev/null +++ b/lib/iris/tests/unit/coord_systems/test_TransverseMercator.py @@ -0,0 +1,58 @@ +# 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.TransverseMercator` class.""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from iris.coord_systems import TransverseMercator + + +class Test_init_defaults(tests.IrisTest): + # NOTE: most of the testing for TransverseMercator is in the legacy test + # module 'iris.tests.test_coordsystem'. + # This class *only* tests the defaults for optional constructor args. + + def test_set_optional_args(self): + # Check that setting the optional (non-ellipse) args works. + crs = TransverseMercator( + 0, + 50, + false_easting=100, + false_northing=-203.7, + scale_factor_at_central_meridian=1.057, + ) + self.assertEqualAndKind(crs.false_easting, 100.0) + self.assertEqualAndKind(crs.false_northing, -203.7) + self.assertEqualAndKind(crs.scale_factor_at_central_meridian, 1.057) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.false_easting, 0.0) + self.assertEqualAndKind(crs.false_northing, 0.0) + self.assertEqualAndKind(crs.scale_factor_at_central_meridian, 1.0) + + def test_no_optional_args(self): + # Check expected defaults with no optional args. + crs = TransverseMercator(0, 50) + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected defaults with optional args=None. + crs = TransverseMercator( + 0, + 50, + false_easting=None, + false_northing=None, + scale_factor_at_central_meridian=None, + ) + self._check_crs_defaults(crs) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/coord_systems/test_VerticalPerspective.py b/lib/iris/tests/unit/coord_systems/test_VerticalPerspective.py index 63ad393c33..155579730e 100644 --- a/lib/iris/tests/unit/coord_systems/test_VerticalPerspective.py +++ b/lib/iris/tests/unit/coord_systems/test_VerticalPerspective.py @@ -57,6 +57,32 @@ def test_projection_creation(self): res = self.vp_cs.as_cartopy_projection() self.assertEqual(res, self.expected) + def test_set_optional_args(self): + # Check that setting the optional (non-ellipse) args works. + crs = VerticalPerspective( + 0, 0, 1000, false_easting=100, false_northing=-203.7 + ) + self.assertEqualAndKind(crs.false_easting, 100.0) + self.assertEqualAndKind(crs.false_northing, -203.7) + + def _check_crs_defaults(self, crs): + # Check for property defaults when no kwargs options were set. + # NOTE: except ellipsoid, which is done elsewhere. + self.assertEqualAndKind(crs.false_easting, 0.0) + self.assertEqualAndKind(crs.false_northing, 0.0) + + def test_no_optional_args(self): + # Check expected defaults with no optional args. + crs = VerticalPerspective(0, 0, 1000) + self._check_crs_defaults(crs) + + def test_optional_args_None(self): + # Check expected defaults with optional args=None. + crs = VerticalPerspective( + 0, 0, 1000, false_easting=None, false_northing=None + ) + self._check_crs_defaults(crs) + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_albers_equal_area_coordinate_system.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_albers_equal_area_coordinate_system.py new file mode 100644 index 0000000000..4d4f719714 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_albers_equal_area_coordinate_system.py @@ -0,0 +1,88 @@ +# 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._pyke_rules.compiled_krb.\ +fc_rules_cf_fc.build_albers_equal_area_coordinate_system`. + +""" + +# import iris tests first so that some things can be initialised before +# importing anything else +import iris.tests as tests + +from unittest import mock + +import iris +from iris.coord_systems import AlbersEqualArea +from iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc import \ + build_albers_equal_area_coordinate_system + + +class TestBuildAlbersEqualAreaCoordinateSystem(tests.IrisTest): + def _test(self, inverse_flattening=False, no_optionals=False): + if no_optionals: + # Most properties are optional for this system. + gridvar_props = {} + # Setup all the expected default values + test_lat = 0 + test_lon = 0 + test_easting = 0 + test_northing = 0 + test_parallels = (20, 50) + else: + # Choose test values and setup corresponding named properties. + test_lat = -35 + test_lon = 175 + test_easting = -100 + test_northing = 200 + test_parallels = (-27, 3) + gridvar_props = dict( + latitude_of_projection_origin=test_lat, + longitude_of_central_meridian=test_lon, + false_easting=test_easting, + false_northing=test_northing, + standard_parallel=test_parallels) + + # Add ellipsoid args. + gridvar_props['semi_major_axis'] = 6377563.396 + if inverse_flattening: + gridvar_props['inverse_flattening'] = 299.3249646 + expected_ellipsoid = iris.coord_systems.GeogCS( + 6377563.396, + inverse_flattening=299.3249646) + else: + gridvar_props['semi_minor_axis'] = 6356256.909 + expected_ellipsoid = iris.coord_systems.GeogCS( + 6377563.396, 6356256.909) + + cf_grid_var = mock.Mock(spec=[], **gridvar_props) + + cs = build_albers_equal_area_coordinate_system(None, cf_grid_var) + + expected = AlbersEqualArea( + latitude_of_projection_origin=test_lat, + longitude_of_central_meridian=test_lon, + false_easting=test_easting, + false_northing=test_northing, + standard_parallels=test_parallels, + ellipsoid=expected_ellipsoid) + + self.assertEqual(cs, expected) + + def test_basic(self): + self._test() + + def test_inverse_flattening(self): + # Check when inverse_flattening is provided instead of semi_minor_axis. + self._test(inverse_flattening=True) + + def test_no_optionals(self): + # Check defaults, when all optional attributes are absent. + self._test(no_optionals=True) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_lambert_azimuthal_equal_area_coordinate_system.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_lambert_azimuthal_equal_area_coordinate_system.py new file mode 100644 index 0000000000..fd3aa189bf --- /dev/null +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_lambert_azimuthal_equal_area_coordinate_system.py @@ -0,0 +1,85 @@ +# 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._pyke_rules.compiled_krb.\ +fc_rules_cf_fc.build_lambert_azimuthal_equal_area_coordinate_system`. + +""" + +# import iris tests first so that some things can be initialised before +# importing anything else +import iris.tests as tests + +from unittest import mock + +import iris +from iris.coord_systems import LambertAzimuthalEqualArea +from iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc import \ + build_lambert_azimuthal_equal_area_coordinate_system + + +class TestBuildLambertAzimuthalEqualAreaCoordinateSystem(tests.IrisTest): + def _test(self, inverse_flattening=False, no_optionals=False): + if no_optionals: + # Most properties are optional for this system. + gridvar_props = {} + # Setup all the expected default values + test_lat = 0 + test_lon = 0 + test_easting = 0 + test_northing = 0 + else: + # Choose test values and setup corresponding named properties. + test_lat = -35 + test_lon = 175 + test_easting = -100 + test_northing = 200 + gridvar_props = dict( + latitude_of_projection_origin=test_lat, + longitude_of_projection_origin=test_lon, + false_easting=test_easting, + false_northing=test_northing) + + # Add ellipsoid args. + gridvar_props['semi_major_axis'] = 6377563.396 + if inverse_flattening: + gridvar_props['inverse_flattening'] = 299.3249646 + expected_ellipsoid = iris.coord_systems.GeogCS( + 6377563.396, + inverse_flattening=299.3249646) + else: + gridvar_props['semi_minor_axis'] = 6356256.909 + expected_ellipsoid = iris.coord_systems.GeogCS( + 6377563.396, 6356256.909) + + cf_grid_var = mock.Mock(spec=[], **gridvar_props) + + cs = build_lambert_azimuthal_equal_area_coordinate_system( + None, cf_grid_var) + + expected = LambertAzimuthalEqualArea( + latitude_of_projection_origin=test_lat, + longitude_of_projection_origin=test_lon, + false_easting=test_easting, + false_northing=test_northing, + ellipsoid=expected_ellipsoid) + + self.assertEqual(cs, expected) + + def test_basic(self): + self._test() + + def test_inverse_flattening(self): + # Check when inverse_flattening is provided instead of semi_minor_axis. + self._test(inverse_flattening=True) + + def test_no_optionals(self): + # Check defaults, when all optional attributes are absent. + self._test(no_optionals=True) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_lambert_conformal_coordinate_system.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_lambert_conformal_coordinate_system.py new file mode 100644 index 0000000000..f56b3d5f14 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_lambert_conformal_coordinate_system.py @@ -0,0 +1,88 @@ +# 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._pyke_rules.compiled_krb.\ +fc_rules_cf_fc.build_lambert_conformal_coordinate_system`. + +""" + +# import iris tests first so that some things can be initialised before +# importing anything else +import iris.tests as tests + +from unittest import mock + +import iris +from iris.coord_systems import LambertConformal +from iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc import \ + build_lambert_conformal_coordinate_system + + +class TestBuildLambertConformalCoordinateSystem(tests.IrisTest): + def _test(self, inverse_flattening=False, no_optionals=False): + if no_optionals: + # Most properties are optional in this case. + gridvar_props = {} + # Setup all the expected default values + test_lat = 39 + test_lon = -96 + test_easting = 0 + test_northing = 0 + test_parallels = (33, 45) + else: + # Choose test values and setup corresponding named properties. + test_lat = -35 + test_lon = 175 + test_easting = -100 + test_northing = 200 + test_parallels = (-27, 3) + gridvar_props = dict( + latitude_of_projection_origin=test_lat, + longitude_of_central_meridian=test_lon, + false_easting=test_easting, + false_northing=test_northing, + standard_parallel=test_parallels) + + # Add ellipsoid args. + gridvar_props['semi_major_axis'] = 6377563.396 + if inverse_flattening: + gridvar_props['inverse_flattening'] = 299.3249646 + expected_ellipsoid = iris.coord_systems.GeogCS( + 6377563.396, + inverse_flattening=299.3249646) + else: + gridvar_props['semi_minor_axis'] = 6356256.909 + expected_ellipsoid = iris.coord_systems.GeogCS( + 6377563.396, 6356256.909) + + cf_grid_var = mock.Mock(spec=[], **gridvar_props) + + cs = build_lambert_conformal_coordinate_system(None, cf_grid_var) + + expected = LambertConformal( + central_lat=test_lat, + central_lon=test_lon, + false_easting=test_easting, + false_northing=test_northing, + secant_latitudes=test_parallels, + ellipsoid=expected_ellipsoid) + + self.assertEqual(cs, expected) + + def test_basic(self): + self._test() + + def test_inverse_flattening(self): + # Check when inverse_flattening is provided instead of semi_minor_axis. + self._test(inverse_flattening=True) + + def test_no_optionals(self): + # Check defaults, when all optional attributes are absent. + self._test(no_optionals=True) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_mercator_coordinate_system.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_mercator_coordinate_system.py index 665beb8747..f59db6ef91 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_mercator_coordinate_system.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_mercator_coordinate_system.py @@ -56,6 +56,20 @@ def test_inverse_flattening(self): inverse_flattening=cf_grid_var.inverse_flattening)) self.assertEqual(cs, expected) + def test_longitude_missing(self): + cf_grid_var = mock.Mock( + spec=[], + semi_major_axis=6377563.396, + inverse_flattening=299.3249646) + + cs = build_mercator_coordinate_system(None, cf_grid_var) + + expected = Mercator( + ellipsoid=iris.coord_systems.GeogCS( + cf_grid_var.semi_major_axis, + inverse_flattening=cf_grid_var.inverse_flattening)) + self.assertEqual(cs, expected) + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_stereographic_coordinate_system.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_stereographic_coordinate_system.py index e95f286a8d..791a0d6014 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_stereographic_coordinate_system.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_stereographic_coordinate_system.py @@ -22,52 +22,57 @@ class TestBuildStereographicCoordinateSystem(tests.IrisTest): - def test_valid(self): - cf_grid_var = mock.Mock( - spec=[], + def _test(self, inverse_flattening=False, no_offsets=False): + test_easting = -100 + test_northing = 200 + gridvar_props = dict( latitude_of_projection_origin=0, longitude_of_projection_origin=0, - false_easting=-100, - false_northing=200, + false_easting=test_easting, + false_northing=test_northing, scale_factor_at_projection_origin=1, - semi_major_axis=6377563.396, - semi_minor_axis=6356256.909) + semi_major_axis=6377563.396) - cs = build_stereographic_coordinate_system(None, cf_grid_var) + if inverse_flattening: + gridvar_props['inverse_flattening'] = 299.3249646 + expected_ellipsoid = iris.coord_systems.GeogCS( + 6377563.396, + inverse_flattening=299.3249646) + else: + gridvar_props['semi_minor_axis'] = 6356256.909 + expected_ellipsoid = iris.coord_systems.GeogCS( + 6377563.396, 6356256.909) - expected = Stereographic( - central_lat=cf_grid_var.latitude_of_projection_origin, - central_lon=cf_grid_var.longitude_of_projection_origin, - false_easting=cf_grid_var.false_easting, - false_northing=cf_grid_var.false_northing, - ellipsoid=iris.coord_systems.GeogCS( - cf_grid_var.semi_major_axis, - cf_grid_var.semi_minor_axis)) - self.assertEqual(cs, expected) + if no_offsets: + del gridvar_props['false_easting'] + del gridvar_props['false_northing'] + test_easting = 0 + test_northing = 0 - def test_inverse_flattening(self): - cf_grid_var = mock.Mock( - spec=[], - latitude_of_projection_origin=0, - longitude_of_projection_origin=0, - false_easting=-100, - false_northing=200, - scale_factor_at_projection_origin=1, - semi_major_axis=6377563.396, - inverse_flattening=299.3249646) + cf_grid_var = mock.Mock(spec=[], **gridvar_props) cs = build_stereographic_coordinate_system(None, cf_grid_var) expected = Stereographic( central_lat=cf_grid_var.latitude_of_projection_origin, central_lon=cf_grid_var.longitude_of_projection_origin, - false_easting=cf_grid_var.false_easting, - false_northing=cf_grid_var.false_northing, - ellipsoid=iris.coord_systems.GeogCS( - cf_grid_var.semi_major_axis, - inverse_flattening=cf_grid_var.inverse_flattening)) + false_easting=test_easting, + false_northing=test_northing, + ellipsoid=expected_ellipsoid) + self.assertEqual(cs, expected) + def test_basic(self): + self._test() + + def test_inverse_flattening(self): + # Check when inverse_flattening is provided instead of semi_minor_axis. + self._test(inverse_flattening=True) + + def test_no_offsets(self): + # Check when false_easting/northing attributes are absent. + self._test(no_offsets=True) + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_transverse_mercator_coordinate_system.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_transverse_mercator_coordinate_system.py new file mode 100644 index 0000000000..62e8996cd1 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_transverse_mercator_coordinate_system.py @@ -0,0 +1,82 @@ +# 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._pyke_rules.compiled_krb.\ +fc_rules_cf_fc.build_transverse_mercator_coordinate_system`. + +""" + +# import iris tests first so that some things can be initialised before +# importing anything else +import iris.tests as tests + +from unittest import mock + +import iris +from iris.coord_systems import TransverseMercator +from iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc import \ + build_transverse_mercator_coordinate_system + + +class TestBuildTransverseMercatorCoordinateSystem(tests.IrisTest): + def _test(self, inverse_flattening=False, no_options=False): + test_easting = -100 + test_northing = 200 + test_scale_factor = 1.234 + gridvar_props = dict( + latitude_of_projection_origin=35.3, + longitude_of_central_meridian=-75, + false_easting=test_easting, + false_northing=test_northing, + scale_factor_at_central_meridian=test_scale_factor, + semi_major_axis=6377563.396) + + if inverse_flattening: + gridvar_props['inverse_flattening'] = 299.3249646 + expected_ellipsoid = iris.coord_systems.GeogCS( + 6377563.396, + inverse_flattening=299.3249646) + else: + gridvar_props['semi_minor_axis'] = 6356256.909 + expected_ellipsoid = iris.coord_systems.GeogCS( + 6377563.396, 6356256.909) + + if no_options: + del gridvar_props['false_easting'] + del gridvar_props['false_northing'] + del gridvar_props['scale_factor_at_central_meridian'] + test_easting = 0 + test_northing = 0 + test_scale_factor = 1.0 + + cf_grid_var = mock.Mock(spec=[], **gridvar_props) + + cs = build_transverse_mercator_coordinate_system(None, cf_grid_var) + + expected = TransverseMercator( + latitude_of_projection_origin=( + cf_grid_var.latitude_of_projection_origin), + longitude_of_central_meridian=( + cf_grid_var.longitude_of_central_meridian), + false_easting=test_easting, + false_northing=test_northing, + scale_factor_at_central_meridian=test_scale_factor, + ellipsoid=expected_ellipsoid) + + self.assertEqual(cs, expected) + + def test_basic(self): + self._test() + + def test_inverse_flattening(self): + self._test(inverse_flattening=True) + + def test_missing_optionals(self): + self._test(no_options=True) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_verticalp_coordinate_system.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_verticalp_coordinate_system.py index d06e93e0ab..090e95e1f3 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_verticalp_coordinate_system.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_verticalp_coordinate_system.py @@ -22,18 +22,20 @@ class TestBuildVerticalPerspectiveCoordinateSystem(tests.IrisTest): - def _test(self, inverse_flattening=False): + def _test(self, inverse_flattening=False, no_offsets=False): """ Generic test that can check vertical perspective validity with or - without inverse flattening. + without inverse flattening, and false_east/northing-s. """ + test_easting = 100.0 + test_northing = 200.0 cf_grid_var_kwargs = { 'spec': [], 'latitude_of_projection_origin': 1.0, 'longitude_of_projection_origin': 2.0, 'perspective_point_height': 2000000.0, - 'false_easting': 100.0, - 'false_northing': 200.0, + 'false_easting': test_easting, + 'false_northing': test_northing, 'semi_major_axis': 6377563.396} ellipsoid_kwargs = {'semi_major_axis': 6377563.396} @@ -43,6 +45,12 @@ def _test(self, inverse_flattening=False): ellipsoid_kwargs['semi_minor_axis'] = 6356256.909 cf_grid_var_kwargs.update(ellipsoid_kwargs) + if no_offsets: + del cf_grid_var_kwargs['false_easting'] + del cf_grid_var_kwargs['false_northing'] + test_easting = 0 + test_northing = 0 + cf_grid_var = mock.Mock(**cf_grid_var_kwargs) ellipsoid = iris.coord_systems.GeogCS(**ellipsoid_kwargs) @@ -53,8 +61,8 @@ def _test(self, inverse_flattening=False): longitude_of_projection_origin=cf_grid_var. longitude_of_projection_origin, perspective_point_height=cf_grid_var.perspective_point_height, - false_easting=cf_grid_var.false_easting, - false_northing=cf_grid_var.false_northing, + false_easting=test_easting, + false_northing=test_northing, ellipsoid=ellipsoid) self.assertEqual(cs, expected) @@ -63,4 +71,13 @@ def test_valid(self): self._test(inverse_flattening=False) def test_inverse_flattening(self): + # Check when inverse_flattening is provided instead of semi_minor_axis. self._test(inverse_flattening=True) + + def test_no_offsets(self): + # Check when false_easting/northing attributes are absent. + self._test(no_offsets=True) + + +if __name__ == "__main__": + tests.main()