From 090dae2937c1848c8b91f38a522d5c927caebf38 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Thu, 13 Jul 2023 14:30:40 +0100 Subject: [PATCH 01/97] Add skeleton for new function in coordinatereference module --- cf/coordinatereference.py | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/cf/coordinatereference.py b/cf/coordinatereference.py index 9601ba1cbd..e7d73d8bd3 100644 --- a/cf/coordinatereference.py +++ b/cf/coordinatereference.py @@ -47,6 +47,50 @@ def _totuple(a): return a +def create_2d_lats_and_lons(): + """TODO.""" + # For keys all Grid Mappings supported by the CF Conventions, as + # listed in Appendix F of the document, given as the specific + # 'grid_mapping_name' attribute string, mapped to values of their PROJ + # 'proj-string' which defines the equivalent coordnate transformtion + # and its parameters under PROJ. + grid_mapping_name_attr_to_proj_string_map = { + # Albers Equal Area + "albers_conical_equal_area": "", + # Azimuthal equidistant + "azimuthal_equidistant": "", + # Geostationary projection + "geostationary": "", + # Lambert azimuthal equal area + "lambert_azimuthal_equal_area": "", + # Lambert conformal + "lambert_conformal_conic": "", + # Lambert Cylindrical Equal Area + "lambert_cylindrical_equal_area": "", + # Latitude-Longitude + "latitude_longitude": "", + # Mercator + "mercator": "", + # Oblique Mercator + "oblique_mercator": "", + # Orthographic + "orthographic": "", + # Polar stereographic + "polar_stereographic": "", + # Rotated pole + "rotated_latitude_longitude": "", + # Sinusoidal + "sinusoidal": "", + # Stereographic + "stereographic": "", + # Transverse Mercator + "transverse_mercator": "", + # Vertical perspective + "vertical_perspective": "", + } + return + + class CoordinateReference(cfdm.CoordinateReference): """A coordinate reference construct of the CF data model. From c716bc74f816bf6a10ab0e8a1e91c3fd28c9123b Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Thu, 13 Jul 2023 14:31:55 +0100 Subject: [PATCH 02/97] Add dict mapping grid mapping names to PROJ proj_string attrs --- cf/coordinatereference.py | 40 +++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/cf/coordinatereference.py b/cf/coordinatereference.py index e7d73d8bd3..37173e7b14 100644 --- a/cf/coordinatereference.py +++ b/cf/coordinatereference.py @@ -56,37 +56,45 @@ def create_2d_lats_and_lons(): # and its parameters under PROJ. grid_mapping_name_attr_to_proj_string_map = { # Albers Equal Area - "albers_conical_equal_area": "", + "albers_conical_equal_area": "aea +lat_1=29.5 +lat_2=42.5", # Azimuthal equidistant - "azimuthal_equidistant": "", + "azimuthal_equidistant": "aeqd", # Geostationary projection - "geostationary": "", + "geostationary": "geos +h=35785831.0 +lon_0=-60 +sweep=y", # Lambert azimuthal equal area - "lambert_azimuthal_equal_area": "", + "lambert_azimuthal_equal_area": "laea", # Lambert conformal - "lambert_conformal_conic": "", + "lambert_conformal_conic": "lcc +lon_0=-90 +lat_1=33 +lat_2=45", # Lambert Cylindrical Equal Area - "lambert_cylindrical_equal_area": "", + # -> note this is called 'Equal Area Cylindrical' in PROJ + "lambert_cylindrical_equal_area": "cea", # Latitude-Longitude - "latitude_longitude": "", + # -> note this is called 'Equidistant Cylindrical (Plate Carrée)' + # in PROJ (TODO: double check this, I think based on + # https://en.wikipedia.org/wiki/Equirectangular_projection it + # is True...) + "latitude_longitude": "eqc", # Mercator - "mercator": "", + "mercator": "merc", # Oblique Mercator - "oblique_mercator": "", + "oblique_mercator": "omerc +lat_1=45 +lat_2=55", # Orthographic - "orthographic": "", + "orthographic": "ortho", # Polar stereographic - "polar_stereographic": "", + "polar_stereographic": "stere +lat_0=90 +lat_ts=75", # Rotated pole - "rotated_latitude_longitude": "", + # -> TODO: not sure what this is in PROJ, could need to use + # latitude_longitude ("eqc") and set extra parameters? + "rotated_latitude_longitude": "???", # Sinusoidal - "sinusoidal": "", + "sinusoidal": "sinu", # Stereographic - "stereographic": "", + "stereographic": "stere +lat_0=90 +lat_ts=75", # Transverse Mercator - "transverse_mercator": "", + "transverse_mercator": "tmerc", # Vertical perspective - "vertical_perspective": "", + # -> TODO: not sure what this is in PROJ. Find out! + "vertical_perspective": "???", } return From 9a5353ca8dbb488f69f0048e9d2a97ebbe92eab2 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Fri, 14 Jul 2023 18:16:51 +0100 Subject: [PATCH 03/97] Add input args + form output constructs for create_2d_lats_and_lons --- cf/coordinatereference.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/cf/coordinatereference.py b/cf/coordinatereference.py index 37173e7b14..a05a90a2b7 100644 --- a/cf/coordinatereference.py +++ b/cf/coordinatereference.py @@ -47,7 +47,7 @@ def _totuple(a): return a -def create_2d_lats_and_lons(): +def create_2d_lats_and_lons(projection, 1d_proj_coors, crs_params): """TODO.""" # For keys all Grid Mappings supported by the CF Conventions, as # listed in Appendix F of the document, given as the specific @@ -96,7 +96,26 @@ def create_2d_lats_and_lons(): # -> TODO: not sure what this is in PROJ. Find out! "vertical_perspective": "???", } - return + + # TODO functional code here to go from inputs to lat_data and lon_data + + # Create latitude construct from data + lat_data = cf.Data(numpy.array(0)) # TODO: minimal dummy data for now + lat_dim = cf.DimensionCoordinate(data=lat_data)) + lat_dim.set_properties( + {"standard_name": "latitude", + "units": "degrees_north"} + ) + + # Create longitude construct from data + lon_data = cf.Data(numpy.array(0)) # TODO: minimal dummy data for now + lon_dim = cf.DimensionCoordinate(data=lon_data) + lon_dim.set_properties( + {"standard_name": "longitude", + "units": "degrees_east"} + ) + + return lat_dim, lon_dim class CoordinateReference(cfdm.CoordinateReference): From df81bb998f5e119bd6ed92cb21c81450b61a0b7d Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Fri, 14 Jul 2023 19:13:40 +0100 Subject: [PATCH 04/97] Update mapping dict to fill in unknowns --- cf/coordinatereference.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cf/coordinatereference.py b/cf/coordinatereference.py index a05a90a2b7..f1465e58f9 100644 --- a/cf/coordinatereference.py +++ b/cf/coordinatereference.py @@ -54,6 +54,8 @@ def create_2d_lats_and_lons(projection, 1d_proj_coors, crs_params): # 'grid_mapping_name' attribute string, mapped to values of their PROJ # 'proj-string' which defines the equivalent coordnate transformtion # and its parameters under PROJ. + # + # TODO double check this mapping to PROJ is correct! grid_mapping_name_attr_to_proj_string_map = { # Albers Equal Area "albers_conical_equal_area": "aea +lat_1=29.5 +lat_2=42.5", @@ -69,10 +71,9 @@ def create_2d_lats_and_lons(projection, 1d_proj_coors, crs_params): # -> note this is called 'Equal Area Cylindrical' in PROJ "lambert_cylindrical_equal_area": "cea", # Latitude-Longitude - # -> note this is called 'Equidistant Cylindrical (Plate Carrée)' - # in PROJ (TODO: double check this, I think based on - # https://en.wikipedia.org/wiki/Equirectangular_projection it - # is True...) + # -> Note this has other names such as 'Equidistant Cylindrical' and + # 'Plate Carrée', see e.g. + # https://en.wikipedia.org/wiki/Equirectangular_projection "latitude_longitude": "eqc", # Mercator "mercator": "merc", @@ -81,11 +82,10 @@ def create_2d_lats_and_lons(projection, 1d_proj_coors, crs_params): # Orthographic "orthographic": "ortho", # Polar stereographic - "polar_stereographic": "stere +lat_0=90 +lat_ts=75", + "polar_stereographic": "ups", # Rotated pole - # -> TODO: not sure what this is in PROJ, could need to use - # latitude_longitude ("eqc") and set extra parameters? - "rotated_latitude_longitude": "???", + # -> need to use latitude_longitude but rotate with extra coors + "rotated_latitude_longitude": "eqc +lat_ts=0 +lon_0=0", # Sinusoidal "sinusoidal": "sinu", # Stereographic @@ -93,8 +93,8 @@ def create_2d_lats_and_lons(projection, 1d_proj_coors, crs_params): # Transverse Mercator "transverse_mercator": "tmerc", # Vertical perspective - # -> TODO: not sure what this is in PROJ. Find out! - "vertical_perspective": "???", + # -> Note in PROJ this comes under 'Near-sided perspective' + "vertical_perspective": "+proj=nsper +h=3000000 +lat_0=-20 +lon_0=145", } # TODO functional code here to go from inputs to lat_data and lon_data From 75fbcebd8d04afb66050a9c2fdd9f03d4f0a94df Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 17 Jul 2023 16:15:13 +0100 Subject: [PATCH 05/97] Create grid mapping module and classes for all supported GMs --- cf/coordinatereference.py | 50 +---- cf/gridmappings.py | 442 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 443 insertions(+), 49 deletions(-) create mode 100644 cf/gridmappings.py diff --git a/cf/coordinatereference.py b/cf/coordinatereference.py index f1465e58f9..7ca7a1a4b3 100644 --- a/cf/coordinatereference.py +++ b/cf/coordinatereference.py @@ -49,59 +49,11 @@ def _totuple(a): def create_2d_lats_and_lons(projection, 1d_proj_coors, crs_params): """TODO.""" - # For keys all Grid Mappings supported by the CF Conventions, as - # listed in Appendix F of the document, given as the specific - # 'grid_mapping_name' attribute string, mapped to values of their PROJ - # 'proj-string' which defines the equivalent coordnate transformtion - # and its parameters under PROJ. - # - # TODO double check this mapping to PROJ is correct! - grid_mapping_name_attr_to_proj_string_map = { - # Albers Equal Area - "albers_conical_equal_area": "aea +lat_1=29.5 +lat_2=42.5", - # Azimuthal equidistant - "azimuthal_equidistant": "aeqd", - # Geostationary projection - "geostationary": "geos +h=35785831.0 +lon_0=-60 +sweep=y", - # Lambert azimuthal equal area - "lambert_azimuthal_equal_area": "laea", - # Lambert conformal - "lambert_conformal_conic": "lcc +lon_0=-90 +lat_1=33 +lat_2=45", - # Lambert Cylindrical Equal Area - # -> note this is called 'Equal Area Cylindrical' in PROJ - "lambert_cylindrical_equal_area": "cea", - # Latitude-Longitude - # -> Note this has other names such as 'Equidistant Cylindrical' and - # 'Plate Carrée', see e.g. - # https://en.wikipedia.org/wiki/Equirectangular_projection - "latitude_longitude": "eqc", - # Mercator - "mercator": "merc", - # Oblique Mercator - "oblique_mercator": "omerc +lat_1=45 +lat_2=55", - # Orthographic - "orthographic": "ortho", - # Polar stereographic - "polar_stereographic": "ups", - # Rotated pole - # -> need to use latitude_longitude but rotate with extra coors - "rotated_latitude_longitude": "eqc +lat_ts=0 +lon_0=0", - # Sinusoidal - "sinusoidal": "sinu", - # Stereographic - "stereographic": "stere +lat_0=90 +lat_ts=75", - # Transverse Mercator - "transverse_mercator": "tmerc", - # Vertical perspective - # -> Note in PROJ this comes under 'Near-sided perspective' - "vertical_perspective": "+proj=nsper +h=3000000 +lat_0=-20 +lon_0=145", - } - # TODO functional code here to go from inputs to lat_data and lon_data # Create latitude construct from data lat_data = cf.Data(numpy.array(0)) # TODO: minimal dummy data for now - lat_dim = cf.DimensionCoordinate(data=lat_data)) + lat_dim = cf.DimensionCoordinate(data=lat_data) lat_dim.set_properties( {"standard_name": "latitude", "units": "degrees_north"} diff --git a/cf/gridmappings.py b/cf/gridmappings.py new file mode 100644 index 0000000000..33ba8a7c3e --- /dev/null +++ b/cf/gridmappings.py @@ -0,0 +1,442 @@ +ALL_GRID_MAPPING_ATTR_NAMES = { + "grid_mapping_name", + # *Those which describe the ellipsoid and prime meridian:* + "earth_radius", + "inverse_flattening", + "longitude_of_prime_meridian", + "prime_meridian_name", + "reference_ellipsoid_name", + "semi_major_axis", + "semi_minor_axis", + # *Specific/applicable to only given grid mapping(s):* + # ...projection origin related: + "longitude_of_projection_origin", + "latitude_of_projection_origin", + "scale_factor_at_projection_origin", + # ...false-Xings: + "false_easting", + "false_northing", + # ...angle axis related: + "sweep_angle_axis", + "fixed_angle_axis", + # ...central meridian related: + "longitude_of_central_meridian", + "scale_factor_at_central_meridian", + # ...pole coordinates related: + "grid_north_pole_latitude", + "grid_north_pole_longitude", + "north_pole_grid_longitude", + # ...other: + "standard_parallel", + "perspective_point_height", + "azimuth_of_central_line", + "straight_vertical_longitude_from_pole", + # *Other, not needed for a specific grid mapping but also listed + # in 'Table F.1. Grid Mapping Attributes':* + "crs_wkt", + "geographic_crs_name", + "geoid_name", + "geopotential_datum_name", + "horizontal_datum_name", + "inverse_flattening", + "projected_crs_name", + "towgs84", + } + + +class GridMapping: + """A container for a Grid Mapping recognised by the CF Conventions.""" + + def __init__( + self, + grid_mapping_name, + proj_id, + earth_radius=None, + inverse_flattening=None, + longitude_of_prime_meridian=None, + prime_meridian_name=None, + reference_ellipsoid_name=None, + semi_major_axis=None, + semi_minor_axis=None, + ): + """**Initialisation** + + :Parameters: + + grid_mapping_name: string + TODO + + proj_id: string + TODO + + earth_radius: number, optional + TODO + + inverse_flattening: TODO, optional + TODO + + longitude_of_prime_meridian: TODO, optional + TODO + + prime_meridian_name: TODO, optional + TODO + + reference_ellipsoid_name: TODO, optional + TODO + + semi_major_axis: TODO, optional + TODO + + semi_minor_axis: TODO, optional + TODO + + """ + raise NotImplementedError( + "Must define a specific Grid Mapping via setting its CF " + "Conventions 'grid_mapping_name' attribute value with the " + "grid_mapping_name parameter, as well as the corresponding " + "base PROJ '+proj' identifier with the proj_id parameter." + ) + + +class AlbersEqualArea(GridMapping): + """The Albers Equal Area grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_albers_equal_area + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/aea.html + + for more information. + + """ + + def __init__(self): + super().__init__("albers_conical_equal_area", "aea") + + +class AzimuthalEquidistant(GridMapping): + """The Azimuthal Equidistant grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#azimuthal-equidistant + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/aeqd.html + + for more information. + + """ + + def __init__(self): + super().__init__("azimuthal_equidistant", "aeqd") + + +class Geostationary(GridMapping): + """The Geostationary Satellite View grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_geostationary_projection + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/geos.html + + for more information. + + """ + + def __init__(self): + super().__init__("geostationary", "geos") + + +class LambertAzimuthalEqualArea(GridMapping): + """The Lambert Azimuthal Equal Area grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#lambert-azimuthal-equal-area + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/laea.html + + for more information. + + """ + + def __init__(self): + super().__init__("lambert_azimuthal_equal_area", "laea") + + +class LambertConformalConic(GridMapping): + """The Lambert Conformal Conic grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_lambert_conformal + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/lcc.html + + for more information. + + """ + + def __init__(self): + super().__init__("lambert_conformal_conic", "lcc") + + +class LambertCylindricalEqualArea(GridMapping): + """The Equal Area Cylindrical grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_lambert_cylindrical_equal_area + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/cea.html + + for more information. + + """ + + def __init__(self): + super().__init__("lambert_cylindrical_equal_area", "cea") + + +class LatitudeLongitude(GridMapping): + """The Latitude-Longitude i.e. Plate Carrée grid mapping. + + For alternative names, see e.g: + + https://en.wikipedia.org/wiki/Equirectangular_projection + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_latitude_longitude + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/eqc.html + + for more information. + + """ + + def __init__(self): + super().__init__("latitude_longitude", "eqc") + + +class Mercator(GridMapping): + """The Mercator grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_mercator + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/merc.html + + for more information. + + """ + + def __init__(self): + super().__init__("mercator", "merc") + + +class ObliqueMercator(GridMapping): + """The Oblique Mercator grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_oblique_mercator + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/omerc.html + + for more information. + + """ + + def __init__(self): + super().__init__("oblique_mercator", "omerc") + + +class Orthographic(GridMapping): + """The Orthographic grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_orthographic + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/ortho.html + + for more information. + + """ + + def __init__(self): + super().__init__("orthographic", "ortho") + + +class PolarStereographic(GridMapping): + """The Universal Polar Stereographic grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#polar-stereographic + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/ups.html + + for more information. + + """ + + def __init__(self): + super().__init__("polar_stereographic", "ups") + + +class RotatedLatitudeLongitude(GridMapping): + """The Rotated Latitude-Longitude grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_rotated_pole + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/eqc.html + + for more information. + + Note: this grid mapping relates to the Latitude-Longitude i.e. Plate + Carrée grid mapping in that it... TODO. + + """ + + def __init__(self): + super().__init__("rotated_latitude_longitude", "eqc") + + +class Sinusoidal(GridMapping): + """The Sinusoidal (Sanson-Flamsteed) grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_sinusoidal + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/sinu.html + + for more information. + + """ + + def __init__(self): + super().__init__("sinusoidal", "sinu") + + +class Stereographic(GridMapping): + """The Stereographic grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_stereographic + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/stere.html + + for more information. + + """ + + def __init__(self): + super().__init__("stereographic", "stere") + + +class TransverseMercator(GridMapping): + """The Transverse Mercator grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_transverse_mercator + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/tmerc.html + + for more information. + + """ + + def __init__(self): + super().__init__("transverse_mercator", "tmerc") + + +class VerticalPerspective(GridMapping): + """The Vertical (or Near-sided) Perspective grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#vertical-perspective + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/nsper.html + + for more information. + + """ + + def __init__(self): + super().__init__("vertical_perspective", "nsper") From 196583931a145d54a48093646402daa7f0621d8b Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Mon, 17 Jul 2023 18:12:07 +0100 Subject: [PATCH 06/97] Create inheritance structure for GMs by classification --- cf/gridmappings.py | 232 +++++++++++++++++++++++++++------------------ 1 file changed, 139 insertions(+), 93 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 33ba8a7c3e..ad144961cb 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -1,47 +1,49 @@ +from pyproj import CRS + ALL_GRID_MAPPING_ATTR_NAMES = { - "grid_mapping_name", - # *Those which describe the ellipsoid and prime meridian:* - "earth_radius", - "inverse_flattening", - "longitude_of_prime_meridian", - "prime_meridian_name", - "reference_ellipsoid_name", - "semi_major_axis", - "semi_minor_axis", - # *Specific/applicable to only given grid mapping(s):* - # ...projection origin related: - "longitude_of_projection_origin", - "latitude_of_projection_origin", - "scale_factor_at_projection_origin", - # ...false-Xings: - "false_easting", - "false_northing", - # ...angle axis related: - "sweep_angle_axis", - "fixed_angle_axis", - # ...central meridian related: - "longitude_of_central_meridian", - "scale_factor_at_central_meridian", - # ...pole coordinates related: - "grid_north_pole_latitude", - "grid_north_pole_longitude", - "north_pole_grid_longitude", - # ...other: - "standard_parallel", - "perspective_point_height", - "azimuth_of_central_line", - "straight_vertical_longitude_from_pole", - # *Other, not needed for a specific grid mapping but also listed - # in 'Table F.1. Grid Mapping Attributes':* - "crs_wkt", - "geographic_crs_name", - "geoid_name", - "geopotential_datum_name", - "horizontal_datum_name", - "inverse_flattening", - "projected_crs_name", - "towgs84", - } + "grid_mapping_name", + # *Those which describe the ellipsoid and prime meridian:* + "earth_radius", + "inverse_flattening", + "longitude_of_prime_meridian", + "prime_meridian_name", + "reference_ellipsoid_name", + "semi_major_axis", + "semi_minor_axis", + # *Specific/applicable to only given grid mapping(s):* + # ...projection origin related: + "longitude_of_projection_origin", + "latitude_of_projection_origin", + "scale_factor_at_projection_origin", + # ...false-Xings: + "false_easting", + "false_northing", + # ...angle axis related: + "sweep_angle_axis", + "fixed_angle_axis", + # ...central meridian related: + "longitude_of_central_meridian", + "scale_factor_at_central_meridian", + # ...pole coordinates related: + "grid_north_pole_latitude", + "grid_north_pole_longitude", + "north_pole_grid_longitude", + # ...other: + "standard_parallel", + "perspective_point_height", + "azimuth_of_central_line", + "straight_vertical_longitude_from_pole", + # *Other, not needed for a specific grid mapping but also listed + # in 'Table F.1. Grid Mapping Attributes':* + "crs_wkt", + "geographic_crs_name", + "geoid_name", + "geopotential_datum_name", + "horizontal_datum_name", + "inverse_flattening", + "projected_crs_name", + "towgs84", +} class GridMapping: @@ -49,8 +51,8 @@ class GridMapping: def __init__( self, - grid_mapping_name, - proj_id, + grid_mapping_name=None, + proj_id=None, earth_radius=None, inverse_flattening=None, longitude_of_prime_meridian=None, @@ -91,15 +93,62 @@ def __init__( TODO """ - raise NotImplementedError( - "Must define a specific Grid Mapping via setting its CF " - "Conventions 'grid_mapping_name' attribute value with the " - "grid_mapping_name parameter, as well as the corresponding " - "base PROJ '+proj' identifier with the proj_id parameter." - ) + if not grid_mapping_name and not proj_id: + raise NotImplementedError( + "Must define a specific Grid Mapping via setting its CF " + "Conventions 'grid_mapping_name' attribute value with the " + "grid_mapping_name parameter, as well as the corresponding " + "base PROJ '+proj' identifier with the proj_id parameter." + ) + + +class AzimuthalGridMapping(GridMapping): + """TODO.""" + + def __init__( + self, longitude_of_projection_origin, latitude_of_projection_origin + ): + super().__init__() + + +class ConicGridMapping(GridMapping): + """TODO.""" + + def __init__(self, standard_parallel, longitude_of_central_meridian): + super().__init__() + +class CylindricalGridMapping(GridMapping): + """TODO.""" -class AlbersEqualArea(GridMapping): + def __init__(self, false_easting, false_northing): + super().__init__() + + +class LatLonGridMapping(GridMapping): + """TODO.""" + + def __init__(self): + super().__init__() + + +class PerspectiveGridMapping(AzimuthalGridMapping): + """TODO.""" + + def __init__( + self, false_easting, false_northing, perspective_point_height + ): + super().__init__() + + +class StereographicGridMapping(AzimuthalGridMapping): + """TODO.""" + + def __init__(self, false_easting, false_northing): + super().__init__() + + +class AlbersEqualArea(ConicGridMapping): """The Albers Equal Area grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -120,7 +169,7 @@ def __init__(self): super().__init__("albers_conical_equal_area", "aea") -class AzimuthalEquidistant(GridMapping): +class AzimuthalEquidistant(AzimuthalGridMapping): """The Azimuthal Equidistant grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -141,7 +190,7 @@ def __init__(self): super().__init__("azimuthal_equidistant", "aeqd") -class Geostationary(GridMapping): +class Geostationary(PerspectiveGridMapping): """The Geostationary Satellite View grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -162,7 +211,7 @@ def __init__(self): super().__init__("geostationary", "geos") -class LambertAzimuthalEqualArea(GridMapping): +class LambertAzimuthalEqualArea(AzimuthalGridMapping): """The Lambert Azimuthal Equal Area grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -183,7 +232,7 @@ def __init__(self): super().__init__("lambert_azimuthal_equal_area", "laea") -class LambertConformalConic(GridMapping): +class LambertConformalConic(ConicGridMapping): """The Lambert Conformal Conic grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -204,7 +253,7 @@ def __init__(self): super().__init__("lambert_conformal_conic", "lcc") -class LambertCylindricalEqualArea(GridMapping): +class LambertCylindricalEqualArea(CylindricalGridMapping): """The Equal Area Cylindrical grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -225,32 +274,7 @@ def __init__(self): super().__init__("lambert_cylindrical_equal_area", "cea") -class LatitudeLongitude(GridMapping): - """The Latitude-Longitude i.e. Plate Carrée grid mapping. - - For alternative names, see e.g: - - https://en.wikipedia.org/wiki/Equirectangular_projection - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_latitude_longitude - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/eqc.html - - for more information. - - """ - - def __init__(self): - super().__init__("latitude_longitude", "eqc") - - -class Mercator(GridMapping): +class Mercator(CylindricalGridMapping): """The Mercator grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -271,7 +295,7 @@ def __init__(self): super().__init__("mercator", "merc") -class ObliqueMercator(GridMapping): +class ObliqueMercator(CylindricalGridMapping): """The Oblique Mercator grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -292,7 +316,7 @@ def __init__(self): super().__init__("oblique_mercator", "omerc") -class Orthographic(GridMapping): +class Orthographic(AzimuthalGridMapping): """The Orthographic grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -313,7 +337,7 @@ def __init__(self): super().__init__("orthographic", "ortho") -class PolarStereographic(GridMapping): +class PolarStereographic(StereographicGridMapping): """The Universal Polar Stereographic grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -334,7 +358,7 @@ def __init__(self): super().__init__("polar_stereographic", "ups") -class RotatedLatitudeLongitude(GridMapping): +class RotatedLatitudeLongitude(LatLonGridMapping): """The Rotated Latitude-Longitude grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -349,15 +373,37 @@ class RotatedLatitudeLongitude(GridMapping): for more information. - Note: this grid mapping relates to the Latitude-Longitude i.e. Plate - Carrée grid mapping in that it... TODO. - """ def __init__(self): super().__init__("rotated_latitude_longitude", "eqc") +class LatitudeLongitude(RotatedLatitudeLongitude): + """The Latitude-Longitude i.e. Plate Carrée grid mapping. + + For alternative names, see e.g: + + https://en.wikipedia.org/wiki/Equirectangular_projection + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_latitude_longitude + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/eqc.html + + for more information. + + """ + + def __init__(self): + super().__init__("latitude_longitude", "eqc") + + class Sinusoidal(GridMapping): """The Sinusoidal (Sanson-Flamsteed) grid mapping. @@ -379,7 +425,7 @@ def __init__(self): super().__init__("sinusoidal", "sinu") -class Stereographic(GridMapping): +class Stereographic(StereographicGridMapping): """The Stereographic grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -400,7 +446,7 @@ def __init__(self): super().__init__("stereographic", "stere") -class TransverseMercator(GridMapping): +class TransverseMercator(CylindricalGridMapping): """The Transverse Mercator grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -421,7 +467,7 @@ def __init__(self): super().__init__("transverse_mercator", "tmerc") -class VerticalPerspective(GridMapping): +class VerticalPerspective(PerspectiveGridMapping): """The Vertical (or Near-sided) Perspective grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on From 6d60ff2f804fe2a0ed22f3fa387ed73cc334d22e Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 18 Jul 2023 13:13:14 +0100 Subject: [PATCH 07/97] Add mapping parameters to all Grid Mapping classes --- cf/gridmappings.py | 235 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 191 insertions(+), 44 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index ad144961cb..190b6ce3e0 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -106,46 +106,57 @@ class AzimuthalGridMapping(GridMapping): """TODO.""" def __init__( - self, longitude_of_projection_origin, latitude_of_projection_origin + self, + longitude_of_projection_origin, + latitude_of_projection_origin, + *args, + **kwargs, ): - super().__init__() + super().__init__(*args, **kwargs) class ConicGridMapping(GridMapping): """TODO.""" - def __init__(self, standard_parallel, longitude_of_central_meridian): - super().__init__() + def __init__( + self, standard_parallel, longitude_of_central_meridian, *args, **kwargs + ): + super().__init__(*args, **kwargs) class CylindricalGridMapping(GridMapping): """TODO.""" - def __init__(self, false_easting, false_northing): - super().__init__() + def __init__(self, false_easting, false_northing, *args, **kwargs): + super().__init__(*args, **kwargs) class LatLonGridMapping(GridMapping): """TODO.""" - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) class PerspectiveGridMapping(AzimuthalGridMapping): """TODO.""" def __init__( - self, false_easting, false_northing, perspective_point_height + self, + false_easting, + false_northing, + perspective_point_height, + *args, + **kwargs, ): - super().__init__() + super().__init__(*args, **kwargs) class StereographicGridMapping(AzimuthalGridMapping): """TODO.""" - def __init__(self, false_easting, false_northing): - super().__init__() + def __init__(self, false_easting, false_northing, *args, **kwargs): + super().__init__(*args, **kwargs) class AlbersEqualArea(ConicGridMapping): @@ -165,8 +176,17 @@ class AlbersEqualArea(ConicGridMapping): """ - def __init__(self): - super().__init__("albers_conical_equal_area", "aea") + def __init__( + self, + standard_parallel, + longitude_of_central_meridian, + atitude_of_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("albers_conical_equal_area", "aea", *args, **kwargs) class AzimuthalEquidistant(AzimuthalGridMapping): @@ -186,8 +206,16 @@ class AzimuthalEquidistant(AzimuthalGridMapping): """ - def __init__(self): - super().__init__("azimuthal_equidistant", "aeqd") + def __init__( + self, + longitude_of_projection_origin, + latitude_of_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("azimuthal_equidistant", "aeqd", *args, **kwargs) class Geostationary(PerspectiveGridMapping): @@ -207,8 +235,19 @@ class Geostationary(PerspectiveGridMapping): """ - def __init__(self): - super().__init__("geostationary", "geos") + def __init__( + self, + latitude_of_projection_origin, + longitude_of_projection_origin, + perspective_point_height, + false_easting, + false_northing, + sweep_angle_axis, + fixed_angle_axis, + *args, + **kwargs, + ): + super().__init__("geostationary", "geos", *args, **kwargs) class LambertAzimuthalEqualArea(AzimuthalGridMapping): @@ -228,8 +267,18 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): """ - def __init__(self): - super().__init__("lambert_azimuthal_equal_area", "laea") + def __init__( + self, + longitude_of_projection_origin, + latitude_of_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__( + "lambert_azimuthal_equal_area", "laea", *args, **kwargs + ) class LambertConformalConic(ConicGridMapping): @@ -249,8 +298,17 @@ class LambertConformalConic(ConicGridMapping): """ - def __init__(self): - super().__init__("lambert_conformal_conic", "lcc") + def __init__( + self, + standard_parallel, + longitude_of_central_meridian, + latitude_of_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("lambert_conformal_conic", "lcc", *args, **kwargs) class LambertCylindricalEqualArea(CylindricalGridMapping): @@ -270,8 +328,19 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): """ - def __init__(self): - super().__init__("lambert_cylindrical_equal_area", "cea") + def __init__( + self, + longitude_of_central_meridian, + standard_parallel, + scale_factor_at_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__( + "lambert_cylindrical_equal_area", "cea", *args, **kwargs + ) class Mercator(CylindricalGridMapping): @@ -291,8 +360,17 @@ class Mercator(CylindricalGridMapping): """ - def __init__(self): - super().__init__("mercator", "merc") + def __init__( + self, + longitude_of_projection_origin, + standard_parallel, + scale_factor_at_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("mercator", "merc", *args, **kwargs) class ObliqueMercator(CylindricalGridMapping): @@ -312,8 +390,18 @@ class ObliqueMercator(CylindricalGridMapping): """ - def __init__(self): - super().__init__("oblique_mercator", "omerc") + def __init__( + self, + azimuth_of_central_line, + latitude_of_projection_origin, + longitude_of_projection_origin, + scale_factor_at_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("oblique_mercator", "omerc", *args, **kwargs) class Orthographic(AzimuthalGridMapping): @@ -333,8 +421,16 @@ class Orthographic(AzimuthalGridMapping): """ - def __init__(self): - super().__init__("orthographic", "ortho") + def __init__( + self, + longitude_of_projection_origin, + latitude_of_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("orthographic", "ortho", *args, **kwargs) class PolarStereographic(StereographicGridMapping): @@ -354,8 +450,18 @@ class PolarStereographic(StereographicGridMapping): """ - def __init__(self): - super().__init__("polar_stereographic", "ups") + def __init__( + self, + straight_vertical_longitude_from_pole, + latitude_of_projection_origin, + standard_parallel, + scale_factor_at_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("polar_stereographic", "ups", *args, **kwargs) class RotatedLatitudeLongitude(LatLonGridMapping): @@ -375,8 +481,15 @@ class RotatedLatitudeLongitude(LatLonGridMapping): """ - def __init__(self): - super().__init__("rotated_latitude_longitude", "eqc") + def __init__( + self, + grid_north_pole_latitude, + grid_north_pole_longitude, + north_pole_grid_longitude, + *args, + **kwargs, + ): + super().__init__("rotated_latitude_longitude", "eqc", *args, **kwargs) class LatitudeLongitude(RotatedLatitudeLongitude): @@ -400,8 +513,8 @@ class LatitudeLongitude(RotatedLatitudeLongitude): """ - def __init__(self): - super().__init__("latitude_longitude", "eqc") + def __init__(self, *args, **kwargs): + super().__init__("latitude_longitude", "eqc", *args, **kwargs) class Sinusoidal(GridMapping): @@ -421,8 +534,15 @@ class Sinusoidal(GridMapping): """ - def __init__(self): - super().__init__("sinusoidal", "sinu") + def __init__( + self, + longitude_of_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("sinusoidal", "sinu", *args, **kwargs) class Stereographic(StereographicGridMapping): @@ -442,8 +562,17 @@ class Stereographic(StereographicGridMapping): """ - def __init__(self): - super().__init__("stereographic", "stere") + def __init__( + self, + longitude_of_projection_origin, + latitude_of_projection_origin, + scale_factor_at_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("stereographic", "stere", *args, **kwargs) class TransverseMercator(CylindricalGridMapping): @@ -463,8 +592,17 @@ class TransverseMercator(CylindricalGridMapping): """ - def __init__(self): - super().__init__("transverse_mercator", "tmerc") + def __init__( + self, + scale_factor_at_central_meridian, + longitude_of_central_meridian, + latitude_of_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("transverse_mercator", "tmerc", *args, **kwargs) class VerticalPerspective(PerspectiveGridMapping): @@ -484,5 +622,14 @@ class VerticalPerspective(PerspectiveGridMapping): """ - def __init__(self): - super().__init__("vertical_perspective", "nsper") + def __init__( + self, + latitude_of_projection_origin, + longitude_of_projection_origin, + perspective_point_height, + false_easting, + false_northing, + *args, + **kwargs, + ): + super().__init__("vertical_perspective", "nsper", *args, **kwargs) From 638b6f0835eccf4d6f8553b51757d4673774aa52 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 18 Jul 2023 15:00:42 +0100 Subject: [PATCH 08/97] Inheritance from abstract to concrete Grid Mapping classes --- cf/gridmappings.py | 219 +++++++++++++++++++++++++++++++++------------ 1 file changed, 163 insertions(+), 56 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 190b6ce3e0..7aab8cc1e6 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -3,23 +3,23 @@ ALL_GRID_MAPPING_ATTR_NAMES = { "grid_mapping_name", # *Those which describe the ellipsoid and prime meridian:* - "earth_radius", - "inverse_flattening", + "earth_radius", # PROJ +R + "inverse_flattening", # PROJ "+rf" "longitude_of_prime_meridian", - "prime_meridian_name", - "reference_ellipsoid_name", - "semi_major_axis", - "semi_minor_axis", + "prime_meridian_name", # PROJ +pm + "reference_ellipsoid_name", # PROJ +ellps + "semi_major_axis", # PROJ "+a" + "semi_minor_axis", # PROJ "+b" # *Specific/applicable to only given grid mapping(s):* # ...projection origin related: - "longitude_of_projection_origin", - "latitude_of_projection_origin", - "scale_factor_at_projection_origin", + "longitude_of_projection_origin", # PROJ +lon_0 + "latitude_of_projection_origin", # PROJ +lat_0 + "scale_factor_at_projection_origin", # PROJ k_0 # ...false-Xings: - "false_easting", - "false_northing", + "false_easting", # PROJ +x_0 + "false_northing", # PROJ +y_0 # ...angle axis related: - "sweep_angle_axis", + "sweep_angle_axis", # PROJ +sweep "fixed_angle_axis", # ...central meridian related: "longitude_of_central_meridian", @@ -29,23 +29,24 @@ "grid_north_pole_longitude", "north_pole_grid_longitude", # ...other: - "standard_parallel", - "perspective_point_height", - "azimuth_of_central_line", - "straight_vertical_longitude_from_pole", + "standard_parallel", # PROJ ["+lat_1", "+lat_2"] (up to 2) + "perspective_point_height", # PROJ "+h" + "azimuth_of_central_line", # PROJ +gamma OR +alpha + "straight_vertical_longitude_from_pole", # PROJ +south # *Other, not needed for a specific grid mapping but also listed # in 'Table F.1. Grid Mapping Attributes':* - "crs_wkt", - "geographic_crs_name", - "geoid_name", + "crs_wkt" "geographic_crs_name", # PROJ "crs_wkt", # PROJ geoid_crs + "geoid_name", # PROJ geoidgrids "geopotential_datum_name", "horizontal_datum_name", - "inverse_flattening", "projected_crs_name", - "towgs84", + "towgs84", # PROJ +towgs84 } +"""Abstract classes for general Grid Mappings.""" + + class GridMapping: """A container for a Grid Mapping recognised by the CF Conventions.""" @@ -69,7 +70,7 @@ def __init__( TODO proj_id: string - TODO + TODO "_proj" projection name earth_radius: number, optional TODO @@ -101,62 +102,109 @@ def __init__( "base PROJ '+proj' identifier with the proj_id parameter." ) + # Defining the Grid Mapping + self.grid_mapping_name = grid_mapping_name + self.proj_id = proj_id + + # The attributes which describe the ellipsoid and prime meridian, + # which may be included, when applicable, with any grid mapping + self.earth_radius = earth_radius + self.inverse_flattening = inverse_flattening + self.longitude_of_prime_meridian = longitude_of_prime_meridian + self.prime_meridian_name = prime_meridian_name + self.reference_ellipsoid_name = reference_ellipsoid_name + self.semi_major_axis = semi_major_axis + self.semi_minor_axis = semi_minor_axis + class AzimuthalGridMapping(GridMapping): - """TODO.""" + """A Grid Mapping with Azimuthal classification.""" def __init__( self, longitude_of_projection_origin, latitude_of_projection_origin, + false_easting, + false_northing, *args, **kwargs, ): super().__init__(*args, **kwargs) + self.longitude_of_projection_origin = longitude_of_projection_origin + self.latitude_of_projection_origin = latitude_of_projection_origin + self.false_easting = false_easting + self.false_northing = false_northing + class ConicGridMapping(GridMapping): - """TODO.""" + """A Grid Mapping with Conic classification.""" def __init__( - self, standard_parallel, longitude_of_central_meridian, *args, **kwargs + self, + standard_parallel, + longitude_of_central_meridian, + latitude_of_projection_origin, + false_easting, + false_northing, + *args, + **kwargs, ): super().__init__(*args, **kwargs) + self.standard_parallel = standard_parallel + self.longitude_of_central_meridian = longitude_of_central_meridian + self.latitude_of_projection_origin = latitude_of_projection_origin + self.false_easting = false_easting + self.false_northing = false_northing + class CylindricalGridMapping(GridMapping): - """TODO.""" + """A Grid Mapping with Cylindrical classification.""" def __init__(self, false_easting, false_northing, *args, **kwargs): super().__init__(*args, **kwargs) + self.false_easting = false_easting + self.false_northing = false_northing + class LatLonGridMapping(GridMapping): - """TODO.""" + """A Grid Mapping with Latitude-Longitude nature. + + Such a Grid Mapping is based upon latitude and longitude coordinates + on a spherical Earth, defining the canonical 2D geographical coordinate + system so that the figure of the Earth can be described. + + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) class PerspectiveGridMapping(AzimuthalGridMapping): - """TODO.""" + """A Grid Mapping with Azimuthal classification and perspective view.""" def __init__( self, - false_easting, - false_northing, perspective_point_height, *args, **kwargs, ): super().__init__(*args, **kwargs) + self.perspective_point_height = perspective_point_height -class StereographicGridMapping(AzimuthalGridMapping): - """TODO.""" - def __init__(self, false_easting, false_northing, *args, **kwargs): - super().__init__(*args, **kwargs) +"""Concrete classes for all Grid Mappings supported by the CF Conventions. + +For the full listing, see: + +https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ +cf-conventions.html#appendix-grid-mappings + +from which these classes should be kept consistent and up-to-date. +""" class AlbersEqualArea(ConicGridMapping): @@ -180,7 +228,7 @@ def __init__( self, standard_parallel, longitude_of_central_meridian, - atitude_of_projection_origin, + latitude_of_projection_origin, false_easting, false_northing, *args, @@ -237,11 +285,11 @@ class Geostationary(PerspectiveGridMapping): def __init__( self, - latitude_of_projection_origin, longitude_of_projection_origin, - perspective_point_height, + latitude_of_projection_origin, false_easting, false_northing, + perspective_point_height, sweep_angle_axis, fixed_angle_axis, *args, @@ -249,6 +297,9 @@ def __init__( ): super().__init__("geostationary", "geos", *args, **kwargs) + self.sweep_angle_axis = sweep_angle_axis + self.fixed_angle_axis = fixed_angle_axis + class LambertAzimuthalEqualArea(AzimuthalGridMapping): """The Lambert Azimuthal Equal Area grid mapping. @@ -330,11 +381,11 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): def __init__( self, - longitude_of_central_meridian, - standard_parallel, - scale_factor_at_projection_origin, false_easting, false_northing, + standard_parallel, + longitude_of_central_meridian, + scale_factor_at_projection_origin, *args, **kwargs, ): @@ -342,6 +393,12 @@ def __init__( "lambert_cylindrical_equal_area", "cea", *args, **kwargs ) + self.standard_parallel = standard_parallel + self.longitude_of_central_meridian = longitude_of_central_meridian + self.scale_factor_at_projection_origin = ( + scale_factor_at_projection_origin + ) + class Mercator(CylindricalGridMapping): """The Mercator grid mapping. @@ -362,16 +419,22 @@ class Mercator(CylindricalGridMapping): def __init__( self, - longitude_of_projection_origin, - standard_parallel, - scale_factor_at_projection_origin, false_easting, false_northing, + standard_parallel, + longitude_of_projection_origin, + scale_factor_at_projection_origin, *args, **kwargs, ): super().__init__("mercator", "merc", *args, **kwargs) + self.standard_parallel = standard_parallel + self.longitude_of_projection_origin = longitude_of_projection_origin + self.scale_factor_at_projection_origin = ( + scale_factor_at_projection_origin + ) + class ObliqueMercator(CylindricalGridMapping): """The Oblique Mercator grid mapping. @@ -392,17 +455,24 @@ class ObliqueMercator(CylindricalGridMapping): def __init__( self, + false_easting, + false_northing, azimuth_of_central_line, latitude_of_projection_origin, longitude_of_projection_origin, scale_factor_at_projection_origin, - false_easting, - false_northing, *args, **kwargs, ): super().__init__("oblique_mercator", "omerc", *args, **kwargs) + self.azimuth_of_central_line = azimuth_of_central_line + self.latitude_of_projection_origin = latitude_of_projection_origin + self.longitude_of_projection_origin = longitude_of_projection_origin + self.scale_factor_at_projection_origin = ( + scale_factor_at_projection_origin + ) + class Orthographic(AzimuthalGridMapping): """The Orthographic grid mapping. @@ -433,7 +503,7 @@ def __init__( super().__init__("orthographic", "ortho", *args, **kwargs) -class PolarStereographic(StereographicGridMapping): +class PolarStereographic(AzimuthalGridMapping): """The Universal Polar Stereographic grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -452,17 +522,36 @@ class PolarStereographic(StereographicGridMapping): def __init__( self, - straight_vertical_longitude_from_pole, latitude_of_projection_origin, - standard_parallel, - scale_factor_at_projection_origin, false_easting, false_northing, + standard_parallel, + scale_factor_at_projection_origin, + longitude_of_projection_origin=None, + straight_vertical_longitude_from_pole=None, *args, **kwargs, ): super().__init__("polar_stereographic", "ups", *args, **kwargs) + # See: https://github.com/cf-convention/cf-conventions/issues/445 + if ( + longitude_of_projection_origin + and straight_vertical_longitude_from_pole + ): + raise ValueError( + "Only one of 'longitude_of_projection_origin' and " + "'straight_vertical_longitude_from_pole' can be set." + ) + + self.straight_vertical_longitude_from_pole = ( + straight_vertical_longitude_from_pole + ) + self.standard_parallel = standard_parallel + self.scale_factor_at_projection_origin = ( + scale_factor_at_projection_origin + ) + class RotatedLatitudeLongitude(LatLonGridMapping): """The Rotated Latitude-Longitude grid mapping. @@ -491,8 +580,12 @@ def __init__( ): super().__init__("rotated_latitude_longitude", "eqc", *args, **kwargs) + self.grid_north_pole_latitude = grid_north_pole_latitude + self.grid_north_pole_longitude = grid_north_pole_longitude + self.north_pole_grid_longitude = north_pole_grid_longitude + -class LatitudeLongitude(RotatedLatitudeLongitude): +class LatitudeLongitude(LatLonGridMapping): """The Latitude-Longitude i.e. Plate Carrée grid mapping. For alternative names, see e.g: @@ -544,8 +637,12 @@ def __init__( ): super().__init__("sinusoidal", "sinu", *args, **kwargs) + self.longitude_of_projection_origin = longitude_of_projection_origin + self.false_easting = false_easting + self.false_northing = false_northing -class Stereographic(StereographicGridMapping): + +class Stereographic(AzimuthalGridMapping): """The Stereographic grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on @@ -566,14 +663,18 @@ def __init__( self, longitude_of_projection_origin, latitude_of_projection_origin, - scale_factor_at_projection_origin, false_easting, false_northing, + scale_factor_at_projection_origin, *args, **kwargs, ): super().__init__("stereographic", "stere", *args, **kwargs) + self.scale_factor_at_projection_origin = ( + scale_factor_at_projection_origin + ) + class TransverseMercator(CylindricalGridMapping): """The Transverse Mercator grid mapping. @@ -594,16 +695,22 @@ class TransverseMercator(CylindricalGridMapping): def __init__( self, + false_easting, + false_northing, scale_factor_at_central_meridian, longitude_of_central_meridian, latitude_of_projection_origin, - false_easting, - false_northing, *args, **kwargs, ): super().__init__("transverse_mercator", "tmerc", *args, **kwargs) + self.scale_factor_at_central_meridian = ( + scale_factor_at_central_meridian + ) + self.longitude_of_central_meridian = longitude_of_central_meridian + self.latitude_of_projection_origin = latitude_of_projection_origin + class VerticalPerspective(PerspectiveGridMapping): """The Vertical (or Near-sided) Perspective grid mapping. @@ -624,11 +731,11 @@ class VerticalPerspective(PerspectiveGridMapping): def __init__( self, - latitude_of_projection_origin, longitude_of_projection_origin, - perspective_point_height, + latitude_of_projection_origin, false_easting, false_northing, + perspective_point_height, *args, **kwargs, ): From a3a10215ef07010e11c7064b824a82ba8bfa91d6 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 19 Jul 2023 17:02:39 +0100 Subject: [PATCH 09/97] Document (map) parameters for Grid Mapping classes --- cf/gridmappings.py | 351 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 347 insertions(+), 4 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 7aab8cc1e6..6f1a0de76c 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -118,7 +118,25 @@ def __init__( class AzimuthalGridMapping(GridMapping): - """A Grid Mapping with Azimuthal classification.""" + """A Grid Mapping with Azimuthal classification. + + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + """ def __init__( self, @@ -138,7 +156,28 @@ def __init__( class ConicGridMapping(GridMapping): - """A Grid Mapping with Conic classification.""" + """A Grid Mapping with Conic classification. + + .. versionadded:: GMVER + + :Parameters: + + standard_parallel: TODO + TODO + + longitude_of_central_meridian: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + """ def __init__( self, @@ -160,7 +199,19 @@ def __init__( class CylindricalGridMapping(GridMapping): - """A Grid Mapping with Cylindrical classification.""" + """A Grid Mapping with Cylindrical classification. + + .. versionadded:: GMVER + + :Parameters: + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + """ def __init__(self, false_easting, false_northing, *args, **kwargs): super().__init__(*args, **kwargs) @@ -176,6 +227,8 @@ class LatLonGridMapping(GridMapping): on a spherical Earth, defining the canonical 2D geographical coordinate system so that the figure of the Earth can be described. + .. versionadded:: GMVER + """ def __init__(self, *args, **kwargs): @@ -183,7 +236,16 @@ def __init__(self, *args, **kwargs): class PerspectiveGridMapping(AzimuthalGridMapping): - """A Grid Mapping with Azimuthal classification and perspective view.""" + """A Grid Mapping with Azimuthal classification and perspective view. + + .. versionadded:: GMVER + + :Parameters: + + perspective_point_height: TODO + TODO + + """ def __init__( self, @@ -222,6 +284,25 @@ class AlbersEqualArea(ConicGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + standard_parallel: TODO + TODO + + longitude_of_central_meridian: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + """ def __init__( @@ -252,6 +333,22 @@ class AzimuthalEquidistant(AzimuthalGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + """ def __init__( @@ -281,6 +378,31 @@ class Geostationary(PerspectiveGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + perspective_point_height: TODO + TODO + + sweep_angle_axis: TODO + TODO + + fixed_angle_axis: TODO + TODO + """ def __init__( @@ -316,6 +438,22 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + """ def __init__( @@ -347,6 +485,25 @@ class LambertConformalConic(ConicGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + standard_parallel: TODO + TODO + + longitude_of_central_meridian: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + """ def __init__( @@ -377,6 +534,25 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + standard_parallel: TODO + TODO + + longitude_of_central_meridian: TODO + TODO + + scale_factor_at_projection_origin: TODO + TODO + """ def __init__( @@ -415,6 +591,25 @@ class Mercator(CylindricalGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + standard_parallel: TODO + TODO + + longitude_of_projection_origin: TODO + TODO + + scale_factor_at_projection_origin: TODO + TODO + """ def __init__( @@ -451,6 +646,28 @@ class ObliqueMercator(CylindricalGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + azimuth_of_central_line: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + longitude_of_projection_origin: TODO + TODO + + scale_factor_at_projection_origin: TODO + TODO + """ def __init__( @@ -489,6 +706,22 @@ class Orthographic(AzimuthalGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + """ def __init__( @@ -518,6 +751,31 @@ class PolarStereographic(AzimuthalGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + straight_vertical_longitude_from_pole: TODO + TODO + + longitude_of_projection_origin: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + scale_factor_at_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + standard_parallel: TODO + TODO + """ def __init__( @@ -568,6 +826,19 @@ class RotatedLatitudeLongitude(LatLonGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + grid_north_pole_latitude: TODO + TODO + + grid_north_pole_longitude: TODO + TODO + + north_pole_grid_longitude: TODO + TODO + """ def __init__( @@ -604,6 +875,8 @@ class LatitudeLongitude(LatLonGridMapping): for more information. + .. versionadded:: GMVER + """ def __init__(self, *args, **kwargs): @@ -625,6 +898,19 @@ class Sinusoidal(GridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + """ def __init__( @@ -657,6 +943,25 @@ class Stereographic(AzimuthalGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + scale_factor_at_projection_origin: TODO + TODO + """ def __init__( @@ -691,6 +996,25 @@ class TransverseMercator(CylindricalGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + scale_factor_at_central_meridian: TODO + TODO + + longitude_of_central_meridian: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + """ def __init__( @@ -727,6 +1051,25 @@ class VerticalPerspective(PerspectiveGridMapping): for more information. + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: TODO + TODO + + latitude_of_projection_origin: TODO + TODO + + false_easting: TODO + TODO + + false_northing: TODO + TODO + + perspective_point_height: TODO + TODO + """ def __init__( From 5542189446f388c618c72a1edafa109be02f345d Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 19 Jul 2023 17:58:19 +0100 Subject: [PATCH 10/97] Document false_* parameters for Grid Mapping classes --- cf/gridmappings.py | 236 ++++++++++++++++++++++++++------------------- 1 file changed, 135 insertions(+), 101 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 6f1a0de76c..2151bd1a3e 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -130,11 +130,13 @@ class AzimuthalGridMapping(GridMapping): latitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. """ @@ -142,8 +144,8 @@ def __init__( self, longitude_of_projection_origin, latitude_of_projection_origin, - false_easting, - false_northing, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -171,11 +173,13 @@ class ConicGridMapping(GridMapping): latitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. """ @@ -184,8 +188,8 @@ def __init__( standard_parallel, longitude_of_central_meridian, latitude_of_projection_origin, - false_easting, - false_northing, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -205,15 +209,17 @@ class CylindricalGridMapping(GridMapping): :Parameters: - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. """ - def __init__(self, false_easting, false_northing, *args, **kwargs): + def __init__(self, false_easting=0.0, false_northing=0.0, *args, **kwargs): super().__init__(*args, **kwargs) self.false_easting = false_easting @@ -297,11 +303,13 @@ class AlbersEqualArea(ConicGridMapping): latitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. """ @@ -310,8 +318,8 @@ def __init__( standard_parallel, longitude_of_central_meridian, latitude_of_projection_origin, - false_easting, - false_northing, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -343,11 +351,13 @@ class AzimuthalEquidistant(AzimuthalGridMapping): latitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. """ @@ -355,8 +365,8 @@ def __init__( self, longitude_of_projection_origin, latitude_of_projection_origin, - false_easting, - false_northing, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -388,11 +398,13 @@ class Geostationary(PerspectiveGridMapping): latitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. perspective_point_height: TODO TODO @@ -409,11 +421,11 @@ def __init__( self, longitude_of_projection_origin, latitude_of_projection_origin, - false_easting, - false_northing, perspective_point_height, sweep_angle_axis, fixed_angle_axis, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -448,11 +460,13 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): latitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. """ @@ -460,8 +474,8 @@ def __init__( self, longitude_of_projection_origin, latitude_of_projection_origin, - false_easting, - false_northing, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -498,11 +512,13 @@ class LambertConformalConic(ConicGridMapping): latitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. """ @@ -511,8 +527,8 @@ def __init__( standard_parallel, longitude_of_central_meridian, latitude_of_projection_origin, - false_easting, - false_northing, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -538,11 +554,13 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): :Parameters: - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. standard_parallel: TODO TODO @@ -557,11 +575,11 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): def __init__( self, - false_easting, - false_northing, standard_parallel, longitude_of_central_meridian, scale_factor_at_projection_origin, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -595,11 +613,13 @@ class Mercator(CylindricalGridMapping): :Parameters: - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. standard_parallel: TODO TODO @@ -614,11 +634,11 @@ class Mercator(CylindricalGridMapping): def __init__( self, - false_easting, - false_northing, standard_parallel, longitude_of_projection_origin, scale_factor_at_projection_origin, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -650,11 +670,13 @@ class ObliqueMercator(CylindricalGridMapping): :Parameters: - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. azimuth_of_central_line: TODO TODO @@ -672,12 +694,12 @@ class ObliqueMercator(CylindricalGridMapping): def __init__( self, - false_easting, - false_northing, azimuth_of_central_line, latitude_of_projection_origin, longitude_of_projection_origin, scale_factor_at_projection_origin, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -716,11 +738,13 @@ class Orthographic(AzimuthalGridMapping): latitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. """ @@ -728,8 +752,8 @@ def __init__( self, longitude_of_projection_origin, latitude_of_projection_origin, - false_easting, - false_northing, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -767,11 +791,13 @@ class PolarStereographic(AzimuthalGridMapping): scale_factor_at_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. standard_parallel: TODO TODO @@ -781,12 +807,12 @@ class PolarStereographic(AzimuthalGridMapping): def __init__( self, latitude_of_projection_origin, - false_easting, - false_northing, standard_parallel, scale_factor_at_projection_origin, longitude_of_projection_origin=None, straight_vertical_longitude_from_pole=None, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -905,19 +931,21 @@ class Sinusoidal(GridMapping): longitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. """ def __init__( self, longitude_of_projection_origin, - false_easting, - false_northing, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -953,11 +981,13 @@ class Stereographic(AzimuthalGridMapping): latitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. scale_factor_at_projection_origin: TODO TODO @@ -968,9 +998,9 @@ def __init__( self, longitude_of_projection_origin, latitude_of_projection_origin, - false_easting, - false_northing, scale_factor_at_projection_origin, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -1000,11 +1030,13 @@ class TransverseMercator(CylindricalGridMapping): :Parameters: - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. scale_factor_at_central_meridian: TODO TODO @@ -1019,11 +1051,11 @@ class TransverseMercator(CylindricalGridMapping): def __init__( self, - false_easting, - false_northing, scale_factor_at_central_meridian, longitude_of_central_meridian, latitude_of_projection_origin, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): @@ -1061,11 +1093,13 @@ class VerticalPerspective(PerspectiveGridMapping): latitude_of_projection_origin: TODO TODO - false_easting: TODO - TODO + false_easting: number, optional + The false easting (PROJ 'x_0') value, in units of metres. + The default is 0.0. - false_northing: TODO - TODO + false_northing: number, optional + The false northing (PROJ 'y_0') value, in units of metres. + The default is 0.0. perspective_point_height: TODO TODO @@ -1076,9 +1110,9 @@ def __init__( self, longitude_of_projection_origin, latitude_of_projection_origin, - false_easting, - false_northing, perspective_point_height, + false_easting=0.0, + false_northing=0.0, *args, **kwargs, ): From a9e01495304519bd23dbd1eb53b7f410efefb1b4 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Thu, 20 Jul 2023 10:59:44 +0100 Subject: [PATCH 11/97] Document *_at_projection_origin in Grid Mapping classes --- cf/gridmappings.py | 293 ++++++++++++++++++++++++++++++++------------- 1 file changed, 209 insertions(+), 84 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 2151bd1a3e..fc6bb7ba5e 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -124,11 +124,21 @@ class AzimuthalGridMapping(GridMapping): :Parameters: - longitude_of_projection_origin: TODO - TODO - - latitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. + + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -142,8 +152,8 @@ class AzimuthalGridMapping(GridMapping): def __init__( self, - longitude_of_projection_origin, - latitude_of_projection_origin, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -170,8 +180,13 @@ class ConicGridMapping(GridMapping): longitude_of_central_meridian: TODO TODO - latitude_of_projection_origin: TODO - TODO + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -300,8 +315,13 @@ class AlbersEqualArea(ConicGridMapping): longitude_of_central_meridian: TODO TODO - latitude_of_projection_origin: TODO - TODO + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -317,7 +337,7 @@ def __init__( self, standard_parallel, longitude_of_central_meridian, - latitude_of_projection_origin, + latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -345,11 +365,21 @@ class AzimuthalEquidistant(AzimuthalGridMapping): :Parameters: - longitude_of_projection_origin: TODO - TODO - - latitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. + + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -363,8 +393,8 @@ class AzimuthalEquidistant(AzimuthalGridMapping): def __init__( self, - longitude_of_projection_origin, - latitude_of_projection_origin, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -392,11 +422,21 @@ class Geostationary(PerspectiveGridMapping): :Parameters: - longitude_of_projection_origin: TODO - TODO - - latitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. + + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -419,11 +459,11 @@ class Geostationary(PerspectiveGridMapping): def __init__( self, - longitude_of_projection_origin, - latitude_of_projection_origin, perspective_point_height, sweep_angle_axis, fixed_angle_axis, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -454,11 +494,21 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): :Parameters: - longitude_of_projection_origin: TODO - TODO - - latitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. + + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -472,8 +522,8 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): def __init__( self, - longitude_of_projection_origin, - latitude_of_projection_origin, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -506,11 +556,21 @@ class LambertConformalConic(ConicGridMapping): standard_parallel: TODO TODO - longitude_of_central_meridian: TODO - TODO - - latitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. + + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -525,8 +585,8 @@ class LambertConformalConic(ConicGridMapping): def __init__( self, standard_parallel, - longitude_of_central_meridian, - latitude_of_projection_origin, + longitude_of_central_meridian=0.0, + latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -624,8 +684,13 @@ class Mercator(CylindricalGridMapping): standard_parallel: TODO TODO - longitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. scale_factor_at_projection_origin: TODO TODO @@ -635,8 +700,8 @@ class Mercator(CylindricalGridMapping): def __init__( self, standard_parallel, - longitude_of_projection_origin, scale_factor_at_projection_origin, + longitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -681,11 +746,21 @@ class ObliqueMercator(CylindricalGridMapping): azimuth_of_central_line: TODO TODO - latitude_of_projection_origin: TODO - TODO - - longitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. + + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. scale_factor_at_projection_origin: TODO TODO @@ -695,8 +770,8 @@ class ObliqueMercator(CylindricalGridMapping): def __init__( self, azimuth_of_central_line, - latitude_of_projection_origin, - longitude_of_projection_origin, + latitude_of_projection_origin=0.0, + longitude_of_projection_origin=0.0, scale_factor_at_projection_origin, false_easting=0.0, false_northing=0.0, @@ -732,11 +807,21 @@ class Orthographic(AzimuthalGridMapping): :Parameters: - longitude_of_projection_origin: TODO - TODO - - latitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. + + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -750,8 +835,8 @@ class Orthographic(AzimuthalGridMapping): def __init__( self, - longitude_of_projection_origin, - latitude_of_projection_origin, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -782,11 +867,21 @@ class PolarStereographic(AzimuthalGridMapping): straight_vertical_longitude_from_pole: TODO TODO - longitude_of_projection_origin: TODO - TODO - - latitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. + + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. scale_factor_at_projection_origin: TODO TODO @@ -806,10 +901,10 @@ class PolarStereographic(AzimuthalGridMapping): def __init__( self, - latitude_of_projection_origin, standard_parallel, scale_factor_at_projection_origin, - longitude_of_projection_origin=None, + latitude_of_projection_origin=0.0, + longitude_of_projection_origin=0.0, straight_vertical_longitude_from_pole=None, false_easting=0.0, false_northing=0.0, @@ -928,8 +1023,13 @@ class Sinusoidal(GridMapping): :Parameters: - longitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -943,7 +1043,7 @@ class Sinusoidal(GridMapping): def __init__( self, - longitude_of_projection_origin, + longitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -975,11 +1075,21 @@ class Stereographic(AzimuthalGridMapping): :Parameters: - longitude_of_projection_origin: TODO - TODO - - latitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. + + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -996,11 +1106,11 @@ class Stereographic(AzimuthalGridMapping): def __init__( self, - longitude_of_projection_origin, - latitude_of_projection_origin, scale_factor_at_projection_origin, false_easting=0.0, false_northing=0.0, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, *args, **kwargs, ): @@ -1044,8 +1154,13 @@ class TransverseMercator(CylindricalGridMapping): longitude_of_central_meridian: TODO TODO - latitude_of_projection_origin: TODO - TODO + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. """ @@ -1053,7 +1168,7 @@ def __init__( self, scale_factor_at_central_meridian, longitude_of_central_meridian, - latitude_of_projection_origin, + latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -1087,11 +1202,21 @@ class VerticalPerspective(PerspectiveGridMapping): :Parameters: - longitude_of_projection_origin: TODO - TODO - - latitude_of_projection_origin: TODO - TODO + longitude_of_projection_origin: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. + + latitude_of_projection_origin: number or `str`, optional + The latitude of projection center (PROJ 'lat_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -1108,9 +1233,9 @@ class VerticalPerspective(PerspectiveGridMapping): def __init__( self, - longitude_of_projection_origin, - latitude_of_projection_origin, perspective_point_height, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, From a57613bf6c34ae00ca2fbb5661c67d01cf153606 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 21 Jul 2023 15:02:06 +0100 Subject: [PATCH 12/97] Document standard_parallel & perspective_point_height GM args --- cf/gridmappings.py | 152 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 119 insertions(+), 33 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index fc6bb7ba5e..55c5ab98f0 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -174,8 +174,17 @@ class ConicGridMapping(GridMapping): :Parameters: - standard_parallel: TODO - TODO + standard_parallel: number, `str` or 2-`tuple`, optional + The standard parallel values, either the first (PROJ + 'lat_1' value), the second (PROJ 'lat_2' value) or + both, given as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being specified for either. In + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + for both values is 0.0 decimal degrees. longitude_of_central_meridian: TODO TODO @@ -263,8 +272,10 @@ class PerspectiveGridMapping(AzimuthalGridMapping): :Parameters: - perspective_point_height: TODO - TODO + perspective_point_height: number + The height of the view point above the surface (PROJ + 'h') value, for example the height of a satellite above + the Earth, in units of meters. """ @@ -309,8 +320,19 @@ class AlbersEqualArea(ConicGridMapping): :Parameters: - standard_parallel: TODO - TODO + standard_parallel: number, `str` or 2-`tuple`, optional + The standard parallel values, either the first (PROJ + 'lat_1' value), the second (PROJ 'lat_2' value) or + both, given as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being specified for either. In + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. + + The default is (0.0, 0.0), that is 0.0 decimal degrees + for the first and second standard parallel values. longitude_of_central_meridian: TODO TODO @@ -335,8 +357,8 @@ class AlbersEqualArea(ConicGridMapping): def __init__( self, - standard_parallel, longitude_of_central_meridian, + standard_parallel=(0.0, 0.0), latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, @@ -422,6 +444,11 @@ class Geostationary(PerspectiveGridMapping): :Parameters: + perspective_point_height: number + The height of the view point above the surface (PROJ + 'h') value, for example the height of a satellite above + the Earth, in units of meters. + longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding @@ -446,33 +473,54 @@ class Geostationary(PerspectiveGridMapping): The false northing (PROJ 'y_0') value, in units of metres. The default is 0.0. - perspective_point_height: TODO - TODO + sweep_angle_axis: `str`, optional + Sweep angle axis of the viewing instrument, which indicates + the axis on which the view sweeps. Valid options + are "x" and "y". The default is "y". - sweep_angle_axis: TODO - TODO + For more information about the nature of this parameter, see: - fixed_angle_axis: TODO - TODO + https://proj.org/en/9.2/operations/projections/ + geos.html#note-on-sweep-angle + + fixed_angle_axis: `str`, optional + The axis on which the view is fixed. It corresponds to the + inner-gimbal axis of the gimbal view model, whose axis of + rotation moves about the outer-gimbal axis. Valid options + are "x" and "y". The default is "x". + + .. note:: If the fixed_angle_axis is "x", sweep_angle_axis + is "y", and vice versa. """ def __init__( self, perspective_point_height, - sweep_angle_axis, - fixed_angle_axis, longitude_of_projection_origin=0.0, latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, + sweep_angle_axis="y", + fixed_angle_axis="x", *args, **kwargs, ): super().__init__("geostationary", "geos", *args, **kwargs) - self.sweep_angle_axis = sweep_angle_axis - self.fixed_angle_axis = fixed_angle_axis + # sweep_angle_axis must be the opposite (of "x" and "y") to + # fixed_angle_axis. + if (sweep_angle_axis.lower(), fixed_angle_axis.lower()) not in [ + ("x", "y")("y", "x") + ]: + raise ValueError( + "The sweep_angle_axis must be the opposite value, from 'x' " + "and 'y', to the fixed_angle_axis." + ) + + # Values "x" and "y" are not case-sensitive, so convert to lower-case + self.sweep_angle_axis = sweep_angle_axis.lower() + self.fixed_angle_axis = fixed_angle_axis.lower() class LambertAzimuthalEqualArea(AzimuthalGridMapping): @@ -553,8 +601,17 @@ class LambertConformalConic(ConicGridMapping): :Parameters: - standard_parallel: TODO - TODO + standard_parallel: number, `str` or 2-`tuple`, optional + The standard parallel values, either the first (PROJ + 'lat_1' value), the second (PROJ 'lat_2' value) or + both, given as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being specified for either. In + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + for both values is 0.0 decimal degrees. longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in @@ -584,7 +641,7 @@ class LambertConformalConic(ConicGridMapping): def __init__( self, - standard_parallel, + standard_parallel=(0.0, 0.0), longitude_of_central_meridian=0.0, latitude_of_projection_origin=0.0, false_easting=0.0, @@ -622,8 +679,17 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): The false northing (PROJ 'y_0') value, in units of metres. The default is 0.0. - standard_parallel: TODO - TODO + standard_parallel: number, `str` or 2-`tuple`, optional + The standard parallel values, either the first (PROJ + 'lat_1' value), the second (PROJ 'lat_2' value) or + both, given as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being specified for either. In + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + for both values is 0.0 decimal degrees. longitude_of_central_meridian: TODO TODO @@ -635,9 +701,9 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): def __init__( self, - standard_parallel, longitude_of_central_meridian, scale_factor_at_projection_origin, + standard_parallel=(0.0, 0.0), false_easting=0.0, false_northing=0.0, *args, @@ -681,8 +747,17 @@ class Mercator(CylindricalGridMapping): The false northing (PROJ 'y_0') value, in units of metres. The default is 0.0. - standard_parallel: TODO - TODO + standard_parallel: number, `str` or 2-`tuple`, optional + The standard parallel values, either the first (PROJ + 'lat_1' value), the second (PROJ 'lat_2' value) or + both, given as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being specified for either. In + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + for both values is 0.0 decimal degrees. longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in @@ -699,8 +774,8 @@ class Mercator(CylindricalGridMapping): def __init__( self, - standard_parallel, scale_factor_at_projection_origin, + standard_parallel=(0.0, 0.0), longitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, @@ -769,10 +844,10 @@ class ObliqueMercator(CylindricalGridMapping): def __init__( self, + scale_factor_at_projection_origin, azimuth_of_central_line, latitude_of_projection_origin=0.0, longitude_of_projection_origin=0.0, - scale_factor_at_projection_origin, false_easting=0.0, false_northing=0.0, *args, @@ -894,15 +969,24 @@ class PolarStereographic(AzimuthalGridMapping): The false northing (PROJ 'y_0') value, in units of metres. The default is 0.0. - standard_parallel: TODO - TODO + standard_parallel: number, `str` or 2-`tuple`, optional + The standard parallel values, either the first (PROJ + 'lat_1' value), the second (PROJ 'lat_2' value) or + both, given as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being specified for either. In + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + for both values is 0.0 decimal degrees. """ def __init__( self, - standard_parallel, scale_factor_at_projection_origin, + standard_parallel=(0.0, 0.0), latitude_of_projection_origin=0.0, longitude_of_projection_origin=0.0, straight_vertical_longitude_from_pole=None, @@ -1202,6 +1286,11 @@ class VerticalPerspective(PerspectiveGridMapping): :Parameters: + perspective_point_height: number + The height of the view point above the surface (PROJ + 'h') value, for example the height of a satellite above + the Earth, in units of meters. + longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding @@ -1226,9 +1315,6 @@ class VerticalPerspective(PerspectiveGridMapping): The false northing (PROJ 'y_0') value, in units of metres. The default is 0.0. - perspective_point_height: TODO - TODO - """ def __init__( From 463dd2ecd3945b22300ba55c486e38e78663399f Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 21 Jul 2023 16:35:51 +0100 Subject: [PATCH 13/97] Document abstract Grid Mapping optional map parameters --- cf/gridmappings.py | 67 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 55c5ab98f0..250597b251 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -57,7 +57,7 @@ def __init__( earth_radius=None, inverse_flattening=None, longitude_of_prime_meridian=None, - prime_meridian_name=None, + prime_meridian_name="Greenwich", reference_ellipsoid_name=None, semi_major_axis=None, semi_minor_axis=None, @@ -66,32 +66,67 @@ def __init__( :Parameters: - grid_mapping_name: string - TODO + Parameters to define the grid mapping: + + grid_mapping_name: `str` + The value of the 'grid_mapping_name' attribute + attached to a data variable, for example + "mercator" to indicate the Mercator projection. + + proj_id: `str` + The PROJ projection identifier shorthand name that + corresponds to the specified 'grid_mapping_name' + attribute, for example "merc" for the Mercator + projection. This is the initial component in the + PROJ 'proj-string' to describe the coordinate + transformation. + + .. note:: Do not specify the full 'proj-string' + including parameters, and do not specify + the projection specifier '+proj' as a + prefix. Only give the projection ID. - proj_id: string - TODO "_proj" projection name + Parameters to define the ellipsoid size and shape: earth_radius: number, optional - TODO + The radius of the sphere e.g. Earth (PROJ 'R' value), + in units of meters. The default is TODO. - inverse_flattening: TODO, optional - TODO + If used in conjunction with 'reference_ellipsoid_name', + this parameter takes precedence. + + reference_ellipsoid_name: `str` or `None`, optional + The name of a built-in ellipsoid definition. + The default is `None`. + + If used in conjunction with 'earth_radius', the + 'earth_radius' parameter takes precedence. + + inverse_flattening: number, optional + The reverse flattening of the ellipsoid (PROJ 'rf' + value), :math:`\frac{1}{f}`, where f corresponds to + the flattening value (PROJ 'f' value) for the ellipsoid, + in units of TODO. The default is TODO. longitude_of_prime_meridian: TODO, optional TODO - prime_meridian_name: TODO, optional - TODO + prime_meridian_name: `str`, optional + The name, or the longitude relative to greenwich, of + the prime meridian (PROJ 'pm' value). The default is + "Greenwich". Supported names and corresponding + longitudes are listed at: - reference_ellipsoid_name: TODO, optional - TODO + https://proj.org/en/9.2/usage/ + projections.html#prime-meridian - semi_major_axis: TODO, optional - TODO + semi_major_axis: number, optional + The semi-major axis of the ellipsoid (PROJ 'a' value) + in units of TODO. The default is TODO. - semi_minor_axis: TODO, optional - TODO + semi_minor_axis: number, optional + The semi-minor axis of the ellipsoid (PROJ 'b' value) + in units of TODO. The default is TODO. """ if not grid_mapping_name and not proj_id: From 3b3c1ca89065422c861e46932073e93df9ffed91 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 31 Jul 2023 14:56:54 +0100 Subject: [PATCH 14/97] Document Grid Mapping scale_factor_at_projection_origin parameter --- cf/gridmappings.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 250597b251..7d7c2077e1 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -729,18 +729,19 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): longitude_of_central_meridian: TODO TODO - scale_factor_at_projection_origin: TODO - TODO + scale_factor_at_projection_origin: number, optional + The scale factor at natural origin (PROJ 'k_0' value). It + is unitless. The default is 1.0. """ def __init__( self, longitude_of_central_meridian, - scale_factor_at_projection_origin, standard_parallel=(0.0, 0.0), false_easting=0.0, false_northing=0.0, + scale_factor_at_projection_origin=1.0, *args, **kwargs, ): @@ -802,18 +803,19 @@ class Mercator(CylindricalGridMapping): of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. - scale_factor_at_projection_origin: TODO - TODO + scale_factor_at_projection_origin: number, optional + The scale factor at natural origin (PROJ 'k_0' value). It + is unitless. The default is 1.0. """ def __init__( self, - scale_factor_at_projection_origin, standard_parallel=(0.0, 0.0), longitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, + scale_factor_at_projection_origin=1.0, *args, **kwargs, ): @@ -872,19 +874,20 @@ class ObliqueMercator(CylindricalGridMapping): of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. - scale_factor_at_projection_origin: TODO - TODO + scale_factor_at_projection_origin: number, optional + The scale factor at natural origin (PROJ 'k_0' value). It + is unitless. The default is 1.0. """ def __init__( self, - scale_factor_at_projection_origin, azimuth_of_central_line, latitude_of_projection_origin=0.0, longitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, + scale_factor_at_projection_origin=1.0, *args, **kwargs, ): @@ -993,8 +996,9 @@ class PolarStereographic(AzimuthalGridMapping): of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. - scale_factor_at_projection_origin: TODO - TODO + scale_factor_at_projection_origin: number, optional + The scale factor at natural origin (PROJ 'k_0' value). It + is unitless. The default is 1.0. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -1020,13 +1024,13 @@ class PolarStereographic(AzimuthalGridMapping): def __init__( self, - scale_factor_at_projection_origin, standard_parallel=(0.0, 0.0), latitude_of_projection_origin=0.0, longitude_of_projection_origin=0.0, straight_vertical_longitude_from_pole=None, false_easting=0.0, false_northing=0.0, + scale_factor_at_projection_origin=1.0, *args, **kwargs, ): @@ -1218,18 +1222,19 @@ class Stereographic(AzimuthalGridMapping): The false northing (PROJ 'y_0') value, in units of metres. The default is 0.0. - scale_factor_at_projection_origin: TODO - TODO + scale_factor_at_projection_origin: number, optional + The scale factor at natural origin (PROJ 'k_0' value). It + is unitless. The default is 1.0. """ def __init__( self, - scale_factor_at_projection_origin, false_easting=0.0, false_northing=0.0, longitude_of_projection_origin=0.0, latitude_of_projection_origin=0.0, + scale_factor_at_projection_origin=1.0, *args, **kwargs, ): From 9b4181054b65bcaa95e1e6d0823c47e874454819 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 31 Jul 2023 16:57:34 +0100 Subject: [PATCH 15/97] Document Grid Mapping longitude_of_central_meridian parameter --- cf/gridmappings.py | 78 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 7d7c2077e1..471f2f35f9 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -218,11 +218,18 @@ class ConicGridMapping(GridMapping): units of decimal degrees, where forming a string by adding a suffix character can indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - for both values is 0.0 decimal degrees. + of 'd', 'D' or '°' confirm units of decimal degrees. - longitude_of_central_meridian: TODO - TODO + The default is (0.0, 0.0), that is 0.0 decimal degrees + for the first and second standard parallel values. + + longitude_of_central_meridian: number or `str`, optional + The longitude of (natural) origin i.e. central meridian, in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in @@ -244,9 +251,9 @@ class ConicGridMapping(GridMapping): def __init__( self, - standard_parallel, - longitude_of_central_meridian, - latitude_of_projection_origin, + standard_parallel=(0.0, 0.0), + longitude_of_central_meridian=0.0, + latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, *args, @@ -369,8 +376,13 @@ class AlbersEqualArea(ConicGridMapping): The default is (0.0, 0.0), that is 0.0 decimal degrees for the first and second standard parallel values. - longitude_of_central_meridian: TODO - TODO + longitude_of_central_meridian: number or `str`, optional + The longitude of (natural) origin i.e. central meridian, in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in @@ -392,7 +404,7 @@ class AlbersEqualArea(ConicGridMapping): def __init__( self, - longitude_of_central_meridian, + longitude_of_central_meridian=0.0, standard_parallel=(0.0, 0.0), latitude_of_projection_origin=0.0, false_easting=0.0, @@ -645,8 +657,10 @@ class LambertConformalConic(ConicGridMapping): units of decimal degrees, where forming a string by adding a suffix character can indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - for both values is 0.0 decimal degrees. + of 'd', 'D' or '°' confirm units of decimal degrees. + + The default is (0.0, 0.0), that is 0.0 decimal degrees + for the first and second standard parallel values. longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in @@ -723,11 +737,18 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): units of decimal degrees, where forming a string by adding a suffix character can indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - for both values is 0.0 decimal degrees. + of 'd', 'D' or '°' confirm units of decimal degrees. - longitude_of_central_meridian: TODO - TODO + The default is (0.0, 0.0), that is 0.0 decimal degrees + for the first and second standard parallel values. + + longitude_of_central_meridian: number or `str`, optional + The longitude of (natural) origin i.e. central meridian, in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. scale_factor_at_projection_origin: number, optional The scale factor at natural origin (PROJ 'k_0' value). It @@ -737,7 +758,7 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): def __init__( self, - longitude_of_central_meridian, + longitude_of_central_meridian=0.0, standard_parallel=(0.0, 0.0), false_easting=0.0, false_northing=0.0, @@ -792,8 +813,10 @@ class Mercator(CylindricalGridMapping): units of decimal degrees, where forming a string by adding a suffix character can indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - for both values is 0.0 decimal degrees. + of 'd', 'D' or '°' confirm units of decimal degrees. + + The default is (0.0, 0.0), that is 0.0 decimal degrees + for the first and second standard parallel values. longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in @@ -1017,8 +1040,10 @@ class PolarStereographic(AzimuthalGridMapping): units of decimal degrees, where forming a string by adding a suffix character can indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - for both values is 0.0 decimal degrees. + of 'd', 'D' or '°' confirm units of decimal degrees. + + The default is (0.0, 0.0), that is 0.0 decimal degrees + for the first and second standard parallel values. """ @@ -1275,8 +1300,13 @@ class TransverseMercator(CylindricalGridMapping): scale_factor_at_central_meridian: TODO TODO - longitude_of_central_meridian: TODO - TODO + longitude_of_central_meridian: number or `str`, optional + The longitude of (natural) origin i.e. central meridian, in + units of decimal degrees, where forming a string by adding + a suffix character can indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in @@ -1291,7 +1321,7 @@ class TransverseMercator(CylindricalGridMapping): def __init__( self, scale_factor_at_central_meridian, - longitude_of_central_meridian, + longitude_of_central_meridian=0.0, latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, From 81dae17706ca232e6c0261cd0668fdc07c8cfa1c Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 1 Aug 2023 15:51:57 +0100 Subject: [PATCH 16/97] Add default parameters from WGS 84 earth specification --- cf/gridmappings.py | 180 ++++++++++++++++++++++++++------------------- 1 file changed, 105 insertions(+), 75 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 471f2f35f9..a6608e4043 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -43,6 +43,13 @@ "towgs84", # PROJ +towgs84 } +# Define this first since it provides the default for several parameters, +# e.g. WGS1984_CF_ATTR_DEFAULTS.semi_major_axis is 6378137.0, the radius +# of the Earth in metres. Note we use the 'merc' projection to take these +# from since that projection includes all the "latlon" attributes, with +# identical values, as well as further map parameters with standard defaults. +WGS1984_CF_ATTR_DEFAULTS = CRS.from_proj4("+proj=merc").to_cf() + """Abstract classes for general Grid Mappings.""" @@ -54,13 +61,13 @@ def __init__( self, grid_mapping_name=None, proj_id=None, + reference_ellipsoid_name="WGS 84", # other defaults derive from this + semi_major_axis=WGS1984_CF_ATTR_DEFAULTS.semi_major_axis, + semi_minor_axis=WGS1984_CF_ATTR_DEFAULTS.semi_minor_axis, + inverse_flattening=WGS1984_CF_ATTR_DEFAULTS.inverse_flattening, + prime_meridian_name=WGS1984_CF_ATTR_DEFAULTS.prime_meridian_name, + longitude_of_prime_meridian=WGS1984_CF_ATTR_DEFAULTS.longitude_of_prime_meridian, earth_radius=None, - inverse_flattening=None, - longitude_of_prime_meridian=None, - prime_meridian_name="Greenwich", - reference_ellipsoid_name=None, - semi_major_axis=None, - semi_minor_axis=None, ): """**Initialisation** @@ -82,51 +89,67 @@ def __init__( transformation. .. note:: Do not specify the full 'proj-string' - including parameters, and do not specify - the projection specifier '+proj' as a - prefix. Only give the projection ID. + including parameters, since these are + calculated from the class input parameters, + and do not include the projection specifier + '+proj' as a prefix. Only give the + projection ID. Parameters to define the ellipsoid size and shape: - earth_radius: number, optional - The radius of the sphere e.g. Earth (PROJ 'R' value), - in units of meters. The default is TODO. - - If used in conjunction with 'reference_ellipsoid_name', - this parameter takes precedence. - reference_ellipsoid_name: `str` or `None`, optional The name of a built-in ellipsoid definition. - The default is `None`. + The default is "WGS 84". - If used in conjunction with 'earth_radius', the - 'earth_radius' parameter takes precedence. + .. note:: If used in conjunction with 'earth_radius', + the 'earth_radius' parameter takes precedence. inverse_flattening: number, optional The reverse flattening of the ellipsoid (PROJ 'rf' value), :math:`\frac{1}{f}`, where f corresponds to - the flattening value (PROJ 'f' value) for the ellipsoid, - in units of TODO. The default is TODO. - - longitude_of_prime_meridian: TODO, optional - TODO + the flattening value (PROJ 'f' value) for the ellipsoid. + Unitless. The default is 298.257223563. prime_meridian_name: `str`, optional - The name, or the longitude relative to greenwich, of - the prime meridian (PROJ 'pm' value). The default is - "Greenwich". Supported names and corresponding - longitudes are listed at: + A predeclared name to define the prime meridian (PROJ + 'pm' value). The default is "Greenwich". Supported + names and corresponding longitudes are listed at: https://proj.org/en/9.2/usage/ projections.html#prime-meridian - semi_major_axis: number, optional + .. note:: If used in conjunction with + 'longitude_of_prime_meridian', this + parameter takes precedence. + + longitude_of_prime_meridian: `str or `None`, optional + The longitude relative to Greenwich of the + prime meridian. The default is 0.0. + + .. note:: If used in conjunction with + 'prime_meridian_name', the + 'prime_meridian_name' parameter takes + precedence. + + semi_major_axis: number or `None`, optional The semi-major axis of the ellipsoid (PROJ 'a' value) - in units of TODO. The default is TODO. + in units of meters. The default is 6378137.0. - semi_minor_axis: number, optional + semi_minor_axis: number or `None`, optional The semi-minor axis of the ellipsoid (PROJ 'b' value) - in units of TODO. The default is TODO. + in units of meters. The default is 6356752.314245179. + + earth_radius: number or `None`, optional + The radius of the ellipsoid, if a sphere (PROJ 'R' value), + in units of meters. If the ellipsoid is not a sphere, + set as `None`, the default, to indicate that ellipsoid + parameters such as the reference_ellipsoid_name or + semi_major_axis and semi_minor_axis are being set, + since these take precendence. + + .. note:: If used in conjunction with + 'reference_ellipsoid_name', this parameter + takes precedence. """ if not grid_mapping_name and not proj_id: @@ -162,7 +185,7 @@ class AzimuthalGridMapping(GridMapping): longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -170,7 +193,7 @@ class AzimuthalGridMapping(GridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -216,7 +239,7 @@ class ConicGridMapping(GridMapping): the first and then the second in order, where `None` indicates that a value is not being specified for either. In units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. @@ -226,7 +249,7 @@ class ConicGridMapping(GridMapping): longitude_of_central_meridian: number or `str`, optional The longitude of (natural) origin i.e. central meridian, in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -234,7 +257,7 @@ class ConicGridMapping(GridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -369,7 +392,7 @@ class AlbersEqualArea(ConicGridMapping): the first and then the second in order, where `None` indicates that a value is not being specified for either. In units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. @@ -379,7 +402,7 @@ class AlbersEqualArea(ConicGridMapping): longitude_of_central_meridian: number or `str`, optional The longitude of (natural) origin i.e. central meridian, in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -387,7 +410,7 @@ class AlbersEqualArea(ConicGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -437,7 +460,7 @@ class AzimuthalEquidistant(AzimuthalGridMapping): longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -445,7 +468,7 @@ class AzimuthalEquidistant(AzimuthalGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -499,7 +522,7 @@ class Geostationary(PerspectiveGridMapping): longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -507,7 +530,7 @@ class Geostationary(PerspectiveGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -536,8 +559,8 @@ class Geostationary(PerspectiveGridMapping): rotation moves about the outer-gimbal axis. Valid options are "x" and "y". The default is "x". - .. note:: If the fixed_angle_axis is "x", sweep_angle_axis - is "y", and vice versa. + .. note:: If the fixed_angle_axis is "x", sweep_angle_axis + is "y", and vice versa. """ @@ -592,7 +615,7 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -600,7 +623,7 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -655,7 +678,7 @@ class LambertConformalConic(ConicGridMapping): the first and then the second in order, where `None` indicates that a value is not being specified for either. In units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. @@ -665,7 +688,7 @@ class LambertConformalConic(ConicGridMapping): longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -673,7 +696,7 @@ class LambertConformalConic(ConicGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -735,7 +758,7 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): the first and then the second in order, where `None` indicates that a value is not being specified for either. In units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. @@ -745,7 +768,7 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): longitude_of_central_meridian: number or `str`, optional The longitude of (natural) origin i.e. central meridian, in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -811,7 +834,7 @@ class Mercator(CylindricalGridMapping): the first and then the second in order, where `None` indicates that a value is not being specified for either. In units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. @@ -821,7 +844,7 @@ class Mercator(CylindricalGridMapping): longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -878,13 +901,20 @@ class ObliqueMercator(CylindricalGridMapping): The false northing (PROJ 'y_0') value, in units of metres. The default is 0.0. - azimuth_of_central_line: TODO - TODO + azimuth_of_central_line: number or `str`, optional + The azimuth i.e. tilt angle of the centerline clockwise + from north at the center point of the line (PROJ 'alpha' + value), in units of decimal degrees, where + forming a string by adding a suffix character + indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -892,7 +922,7 @@ class ObliqueMercator(CylindricalGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -905,7 +935,7 @@ class ObliqueMercator(CylindricalGridMapping): def __init__( self, - azimuth_of_central_line, + azimuth_of_central_line=0.0, latitude_of_projection_origin=0.0, longitude_of_projection_origin=0.0, false_easting=0.0, @@ -946,7 +976,7 @@ class Orthographic(AzimuthalGridMapping): longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -954,7 +984,7 @@ class Orthographic(AzimuthalGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -1000,13 +1030,13 @@ class PolarStereographic(AzimuthalGridMapping): :Parameters: - straight_vertical_longitude_from_pole: TODO + straight_vertical_longitude_from_pole: TODOSADIES TODO longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -1014,7 +1044,7 @@ class PolarStereographic(AzimuthalGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -1038,7 +1068,7 @@ class PolarStereographic(AzimuthalGridMapping): the first and then the second in order, where `None` indicates that a value is not being specified for either. In units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. @@ -1099,7 +1129,7 @@ class RotatedLatitudeLongitude(LatLonGridMapping): :Parameters: - grid_north_pole_latitude: TODO + grid_north_pole_latitude: TODOSADIES TODO grid_north_pole_longitude: TODO @@ -1174,7 +1204,7 @@ class Sinusoidal(GridMapping): longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -1226,7 +1256,7 @@ class Stereographic(AzimuthalGridMapping): longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -1234,7 +1264,7 @@ class Stereographic(AzimuthalGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -1297,13 +1327,13 @@ class TransverseMercator(CylindricalGridMapping): The false northing (PROJ 'y_0') value, in units of metres. The default is 0.0. - scale_factor_at_central_meridian: TODO + scale_factor_at_central_meridian: TODOSADIES TODO longitude_of_central_meridian: number or `str`, optional The longitude of (natural) origin i.e. central meridian, in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -1311,7 +1341,7 @@ class TransverseMercator(CylindricalGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -1364,7 +1394,7 @@ class VerticalPerspective(PerspectiveGridMapping): longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. @@ -1372,7 +1402,7 @@ class VerticalPerspective(PerspectiveGridMapping): latitude_of_projection_origin: number or `str`, optional The latitude of projection center (PROJ 'lat_0' value), in units of decimal degrees, where forming a string by adding - a suffix character can indicates alternative units of + a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. The default is 0.0 decimal degrees. From 627d65cdda554cbf06c8f8fcd8be5cfbb8d50aa3 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 1 Aug 2023 15:53:04 +0100 Subject: [PATCH 17/97] Set and confirm defaults from 'World Geodetic System 1984' --- cf/gridmappings.py | 190 ++++++++++++++++++++++++++++++--------------- 1 file changed, 129 insertions(+), 61 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index a6608e4043..ce0ee91530 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -3,23 +3,23 @@ ALL_GRID_MAPPING_ATTR_NAMES = { "grid_mapping_name", # *Those which describe the ellipsoid and prime meridian:* - "earth_radius", # PROJ +R - "inverse_flattening", # PROJ "+rf" + "earth_radius", # PROJ '+R' value + "inverse_flattening", # PROJ '+rf' value "longitude_of_prime_meridian", - "prime_meridian_name", # PROJ +pm - "reference_ellipsoid_name", # PROJ +ellps - "semi_major_axis", # PROJ "+a" - "semi_minor_axis", # PROJ "+b" + "prime_meridian_name", # PROJ '+pm' value + "reference_ellipsoid_name", # PROJ '+ellps' value + "semi_major_axis", # PROJ '+a' value + "semi_minor_axis", # PROJ '+b' value # *Specific/applicable to only given grid mapping(s):* # ...projection origin related: - "longitude_of_projection_origin", # PROJ +lon_0 - "latitude_of_projection_origin", # PROJ +lat_0 - "scale_factor_at_projection_origin", # PROJ k_0 + "longitude_of_projection_origin", # PROJ '+lon_0' value + "latitude_of_projection_origin", # PROJ '+lat_0' value + "scale_factor_at_projection_origin", # PROJ '+k_0' value # ...false-Xings: - "false_easting", # PROJ +x_0 - "false_northing", # PROJ +y_0 + "false_easting", # PROJ '+x_0' value + "false_northing", # PROJ '+y_0' value # ...angle axis related: - "sweep_angle_axis", # PROJ +sweep + "sweep_angle_axis", # PROJ '+sweep' value "fixed_angle_axis", # ...central meridian related: "longitude_of_central_meridian", @@ -29,29 +29,68 @@ "grid_north_pole_longitude", "north_pole_grid_longitude", # ...other: - "standard_parallel", # PROJ ["+lat_1", "+lat_2"] (up to 2) - "perspective_point_height", # PROJ "+h" - "azimuth_of_central_line", # PROJ +gamma OR +alpha - "straight_vertical_longitude_from_pole", # PROJ +south + "standard_parallel", # PROJ ['+lat_1', '+lat_2'] values + "perspective_point_height", # PROJ '+h' value + "azimuth_of_central_line", # PROJ '+alpha' (ignore '+gamma') + "straight_vertical_longitude_from_pole", # *Other, not needed for a specific grid mapping but also listed # in 'Table F.1. Grid Mapping Attributes':* - "crs_wkt" "geographic_crs_name", # PROJ "crs_wkt", # PROJ geoid_crs - "geoid_name", # PROJ geoidgrids + "crs_wkt", # PROJ 'crs_wkt' value + "geographic_crs_name", + "geoid_name", "geopotential_datum_name", "horizontal_datum_name", "projected_crs_name", - "towgs84", # PROJ +towgs84 + "towgs84", # PROJ '+towgs84' value } -# Define this first since it provides the default for several parameters, -# e.g. WGS1984_CF_ATTR_DEFAULTS.semi_major_axis is 6378137.0, the radius -# of the Earth in metres. Note we use the 'merc' projection to take these -# from since that projection includes all the "latlon" attributes, with -# identical values, as well as further map parameters with standard defaults. +""" +Define this first since it provides the default for several parameters, +e.g. WGS1984_CF_ATTR_DEFAULTS.semi_major_axis is 6378137.0, the radius +of the Earth in metres. Note we use the 'merc' projection to take these +from since that projection includes all the attributes given for +'latlon' instead and with identical values, but also includes further +map parameters with defaults applied across the projections. + +At the time of dedicating the code, the value of this is as follows, and +the values documented as defaults in the docstrings are taken from this: + +{'crs_wkt': '', + 'semi_major_axis': 6378137.0, + 'semi_minor_axis': 6356752.314245179, + 'inverse_flattening': 298.257223563, + 'reference_ellipsoid_name': 'WGS 84', + 'longitude_of_prime_meridian': 0.0, + 'prime_meridian_name': 'Greenwich', + 'geographic_crs_name': 'unknown', + 'horizontal_datum_name': 'World Geodetic System 1984', + 'projected_crs_name': 'unknown', + 'grid_mapping_name': 'mercator', + 'standard_parallel': 0.0, + 'longitude_of_projection_origin': 0.0, + 'false_easting': 0.0, + 'false_northing': 0.0, + 'scale_factor_at_projection_origin': 1.0} +""" WGS1984_CF_ATTR_DEFAULTS = CRS.from_proj4("+proj=merc").to_cf() -"""Abstract classes for general Grid Mappings.""" +"""Abstract classes for general Grid Mappings. + +Note that default arguments are based upon the PROJ defaults, which can +be cross-referenced via running: + +CRS.from_proj4("+proj= ").to_cf() + +where is for when required arguments must be provided +to return a coordinate reference instance, and obviously these values +where reported should not be included as defaults. An example is: + +CRS.from_proj4("+proj=lcc +lat_1=1").to_cf() + +where `'standard_parallel': (1.0, 0.0)` would not be taken as a default. + +""" class GridMapping: @@ -61,7 +100,8 @@ def __init__( self, grid_mapping_name=None, proj_id=None, - reference_ellipsoid_name="WGS 84", # other defaults derive from this + # i.e. WGS1984_CF_ATTR_DEFAULTS.reference_ellipsoid_name: + reference_ellipsoid_name="WGS 84", semi_major_axis=WGS1984_CF_ATTR_DEFAULTS.semi_major_axis, semi_minor_axis=WGS1984_CF_ATTR_DEFAULTS.semi_minor_axis, inverse_flattening=WGS1984_CF_ATTR_DEFAULTS.inverse_flattening, @@ -232,7 +272,7 @@ class ConicGridMapping(GridMapping): :Parameters: - standard_parallel: number, `str` or 2-`tuple`, optional + standard_parallel: number, `str` or 2-`tuple` The standard parallel values, either the first (PROJ 'lat_1' value), the second (PROJ 'lat_2' value) or both, given as a 2-tuple of numbers or strings corresponding to @@ -274,7 +314,7 @@ class ConicGridMapping(GridMapping): def __init__( self, - standard_parallel=(0.0, 0.0), + standard_parallel, longitude_of_central_meridian=0.0, latitude_of_projection_origin=0.0, false_easting=0.0, @@ -427,8 +467,8 @@ class AlbersEqualArea(ConicGridMapping): def __init__( self, + standard_parallel, longitude_of_central_meridian=0.0, - standard_parallel=(0.0, 0.0), latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, @@ -671,7 +711,7 @@ class LambertConformalConic(ConicGridMapping): :Parameters: - standard_parallel: number, `str` or 2-`tuple`, optional + standard_parallel: number, `str` or 2-`tuple` The standard parallel values, either the first (PROJ 'lat_1' value), the second (PROJ 'lat_2' value) or both, given as a 2-tuple of numbers or strings corresponding to @@ -713,7 +753,7 @@ class LambertConformalConic(ConicGridMapping): def __init__( self, - standard_parallel=(0.0, 0.0), + standard_parallel, longitude_of_central_meridian=0.0, latitude_of_projection_origin=0.0, false_easting=0.0, @@ -762,8 +802,9 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. - The default is (0.0, 0.0), that is 0.0 decimal degrees - for the first and second standard parallel values. + The default is (0.0, None), that is 0.0 decimal degrees + for the first standard parallel value and nothing set for + the second. longitude_of_central_meridian: number or `str`, optional The longitude of (natural) origin i.e. central meridian, in @@ -774,18 +815,18 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): is 0.0 decimal degrees. scale_factor_at_projection_origin: number, optional - The scale factor at natural origin (PROJ 'k_0' value). It - is unitless. The default is 1.0. + The scale factor used in the projection (PROJ 'k_0' value). + It is unitless. The default is 1.0. """ def __init__( self, - longitude_of_central_meridian=0.0, - standard_parallel=(0.0, 0.0), + standard_parallel=(0.0, None), false_easting=0.0, false_northing=0.0, scale_factor_at_projection_origin=1.0, + longitude_of_central_meridian=0.0, *args, **kwargs, ): @@ -838,8 +879,9 @@ class Mercator(CylindricalGridMapping): radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. - The default is (0.0, 0.0), that is 0.0 decimal degrees - for the first and second standard parallel values. + The default is (0.0, None), that is 0.0 decimal degrees + for the first standard parallel value and nothing set for + the second. longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in @@ -850,14 +892,14 @@ class Mercator(CylindricalGridMapping): is 0.0 decimal degrees. scale_factor_at_projection_origin: number, optional - The scale factor at natural origin (PROJ 'k_0' value). It - is unitless. The default is 1.0. + The scale factor used in the projection (PROJ 'k_0' value). + It is unitless. The default is 1.0. """ def __init__( self, - standard_parallel=(0.0, 0.0), + standard_parallel=(0.0, None), longitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, @@ -928,8 +970,8 @@ class ObliqueMercator(CylindricalGridMapping): is 0.0 decimal degrees. scale_factor_at_projection_origin: number, optional - The scale factor at natural origin (PROJ 'k_0' value). It - is unitless. The default is 1.0. + The scale factor used in the projection (PROJ 'k_0' value). + It is unitless. The default is 1.0. """ @@ -1030,8 +1072,14 @@ class PolarStereographic(AzimuthalGridMapping): :Parameters: - straight_vertical_longitude_from_pole: TODOSADIES - TODO + straight_vertical_longitude_from_pole: number or `str`, optional + The longitude of (natural) origin i.e. central meridian, + oriented straight up from the North or South Pole, in + units of decimal degrees, where forming a string by adding + a suffix character indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. longitude_of_projection_origin: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in @@ -1050,8 +1098,8 @@ class PolarStereographic(AzimuthalGridMapping): is 0.0 decimal degrees. scale_factor_at_projection_origin: number, optional - The scale factor at natural origin (PROJ 'k_0' value). It - is unitless. The default is 1.0. + The scale factor used in the projection (PROJ 'k_0' value). + It is unitless. The default is 1.0. false_easting: number, optional The false easting (PROJ 'x_0') value, in units of metres. @@ -1082,13 +1130,15 @@ def __init__( standard_parallel=(0.0, 0.0), latitude_of_projection_origin=0.0, longitude_of_projection_origin=0.0, - straight_vertical_longitude_from_pole=None, + straight_vertical_longitude_from_pole=0.0, false_easting=0.0, false_northing=0.0, scale_factor_at_projection_origin=1.0, *args, **kwargs, ): + # TODO check defaults here, they do not appear for + # CRS.from_proj4("+proj=ups").to_cf() to cross reference! super().__init__("polar_stereographic", "ups", *args, **kwargs) # See: https://github.com/cf-convention/cf-conventions/issues/445 @@ -1129,14 +1179,31 @@ class RotatedLatitudeLongitude(LatLonGridMapping): :Parameters: - grid_north_pole_latitude: TODOSADIES - TODO + grid_north_pole_latitude: number or `str` + Latitude of the North pole of the unrotated source CRS, + expressed in the rotated geographic CRS, in + units of decimal degrees, where forming a string by adding + a suffix character indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. - grid_north_pole_longitude: TODO - TODO + grid_north_pole_longitude: number or `str` + Longitude of the North pole of the unrotated source CRS, + expressed in the rotated geographic CRS, in + units of decimal degrees, where forming a string by adding + a suffix character indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. - north_pole_grid_longitude: TODO - TODO + north_pole_grid_longitude: number or `str`, optional + The longitude of projection center (PROJ 'lon_0' value), in + units of decimal degrees, where forming a string by adding + a suffix character indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is 0.0 decimal degrees. """ @@ -1144,7 +1211,7 @@ def __init__( self, grid_north_pole_latitude, grid_north_pole_longitude, - north_pole_grid_longitude, + north_pole_grid_longitude=0.0, *args, **kwargs, ): @@ -1278,8 +1345,8 @@ class Stereographic(AzimuthalGridMapping): The default is 0.0. scale_factor_at_projection_origin: number, optional - The scale factor at natural origin (PROJ 'k_0' value). It - is unitless. The default is 1.0. + The scale factor used in the projection (PROJ 'k_0' value). + It is unitless. The default is 1.0. """ @@ -1327,8 +1394,9 @@ class TransverseMercator(CylindricalGridMapping): The false northing (PROJ 'y_0') value, in units of metres. The default is 0.0. - scale_factor_at_central_meridian: TODOSADIES - TODO + scale_factor_at_central_meridian: number, optional + The scale factor at (natural) origin i.e. central meridian. + It is unitless. The default is 1.0. longitude_of_central_meridian: number or `str`, optional The longitude of (natural) origin i.e. central meridian, in @@ -1350,7 +1418,7 @@ class TransverseMercator(CylindricalGridMapping): def __init__( self, - scale_factor_at_central_meridian, + scale_factor_at_central_meridian=1.0, longitude_of_central_meridian=0.0, latitude_of_projection_origin=0.0, false_easting=0.0, From 3cb45da18845353dbb43b9a97cc8a0aa1cc53607 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Wed, 2 Aug 2023 17:35:52 +0100 Subject: [PATCH 18/97] Update LatLon type Grid Mapping classes proj-string ID --- cf/gridmappings.py | 52 +++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index ce0ee91530..73eaee53fe 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -3,23 +3,23 @@ ALL_GRID_MAPPING_ATTR_NAMES = { "grid_mapping_name", # *Those which describe the ellipsoid and prime meridian:* - "earth_radius", # PROJ '+R' value - "inverse_flattening", # PROJ '+rf' value + "earth_radius", # -------------------- PROJ '+R' value + "inverse_flattening", # -------------- PROJ '+rf' value "longitude_of_prime_meridian", - "prime_meridian_name", # PROJ '+pm' value - "reference_ellipsoid_name", # PROJ '+ellps' value - "semi_major_axis", # PROJ '+a' value - "semi_minor_axis", # PROJ '+b' value + "prime_meridian_name", # ------------- PROJ '+pm' value + "reference_ellipsoid_name", # -------- PROJ '+ellps' value + "semi_major_axis", # ----------------- PROJ '+a' value + "semi_minor_axis", # ----------------- PROJ '+b' value # *Specific/applicable to only given grid mapping(s):* # ...projection origin related: - "longitude_of_projection_origin", # PROJ '+lon_0' value - "latitude_of_projection_origin", # PROJ '+lat_0' value + "longitude_of_projection_origin", # -- PROJ '+lon_0' value + "latitude_of_projection_origin", # --- PROJ '+lat_0' value "scale_factor_at_projection_origin", # PROJ '+k_0' value # ...false-Xings: - "false_easting", # PROJ '+x_0' value - "false_northing", # PROJ '+y_0' value + "false_easting", # ------------------- PROJ '+x_0' value + "false_northing", # ------------------ PROJ '+y_0' value # ...angle axis related: - "sweep_angle_axis", # PROJ '+sweep' value + "sweep_angle_axis", # ---------------- PROJ '+sweep' value "fixed_angle_axis", # ...central meridian related: "longitude_of_central_meridian", @@ -29,19 +29,19 @@ "grid_north_pole_longitude", "north_pole_grid_longitude", # ...other: - "standard_parallel", # PROJ ['+lat_1', '+lat_2'] values - "perspective_point_height", # PROJ '+h' value - "azimuth_of_central_line", # PROJ '+alpha' (ignore '+gamma') + "standard_parallel", # --------------- PROJ ['+lat_1', '+lat_2'] values + "perspective_point_height", # -------- PROJ '+h' value + "azimuth_of_central_line", # --------- PROJ '+alpha' (ignore '+gamma') "straight_vertical_longitude_from_pole", # *Other, not needed for a specific grid mapping but also listed # in 'Table F.1. Grid Mapping Attributes':* - "crs_wkt", # PROJ 'crs_wkt' value + "crs_wkt", # ------------------------- PROJ 'crs_wkt' value "geographic_crs_name", "geoid_name", "geopotential_datum_name", "horizontal_datum_name", "projected_crs_name", - "towgs84", # PROJ '+towgs84' value + "towgs84", # ------------------------- PROJ '+towgs84' value } """ @@ -1169,10 +1169,6 @@ class RotatedLatitudeLongitude(LatLonGridMapping): http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ cf-conventions.html#_rotated_pole - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/eqc.html - for more information. .. versionadded:: GMVER @@ -1215,7 +1211,9 @@ def __init__( *args, **kwargs, ): - super().__init__("rotated_latitude_longitude", "eqc", *args, **kwargs) + super().__init__( + "rotated_latitude_longitude", "latlon", *args, **kwargs + ) self.grid_north_pole_latitude = grid_north_pole_latitude self.grid_north_pole_longitude = grid_north_pole_longitude @@ -1223,11 +1221,7 @@ def __init__( class LatitudeLongitude(LatLonGridMapping): - """The Latitude-Longitude i.e. Plate Carrée grid mapping. - - For alternative names, see e.g: - - https://en.wikipedia.org/wiki/Equirectangular_projection + """The Latitude-Longitude grid mapping. See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on this grid mapping: @@ -1235,10 +1229,6 @@ class LatitudeLongitude(LatLonGridMapping): http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ cf-conventions.html#_latitude_longitude - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/eqc.html - for more information. .. versionadded:: GMVER @@ -1246,7 +1236,7 @@ class LatitudeLongitude(LatLonGridMapping): """ def __init__(self, *args, **kwargs): - super().__init__("latitude_longitude", "eqc", *args, **kwargs) + super().__init__("latitude_longitude", "latlon", *args, **kwargs) class Sinusoidal(GridMapping): From f656b33804c1e3c31bb031c9be6cd3fe0ac5b592 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Wed, 2 Aug 2023 19:22:45 +0100 Subject: [PATCH 19/97] Import gridmapping classes to module and set string repr's --- cf/__init__.py | 25 ++++++++++++++++++++++++ cf/coordinatereference.py | 2 +- cf/gridmappings.py | 40 +++++++++++++++++++++++++++++++-------- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/cf/__init__.py b/cf/__init__.py index c9d8ce1853..6a85a9de62 100644 --- a/cf/__init__.py +++ b/cf/__init__.py @@ -323,6 +323,31 @@ from .regrid import RegridOperator +from .gridmappings import ( + GridMapping, + AzimuthalGridMapping, + ConicGridMapping, + CylindricalGridMapping, + LatLonGridMapping, + PerspectiveGridMapping, + AlbersEqualArea, + AzimuthalEquidistant, + Geostationary, + LambertAzimuthalEqualArea, + LambertConformalConic, + LambertCylindricalEqualArea, + Mercator, + ObliqueMercator, + Orthographic, + PolarStereographic, + RotatedLatitudeLongitude, + LatitudeLongitude, + Sinusoidal, + Stereographic, + TransverseMercator, + VerticalPerspective, +) + # Set up basic logging for the full project with a root logger import logging diff --git a/cf/coordinatereference.py b/cf/coordinatereference.py index 7ca7a1a4b3..3e8f24c021 100644 --- a/cf/coordinatereference.py +++ b/cf/coordinatereference.py @@ -47,7 +47,7 @@ def _totuple(a): return a -def create_2d_lats_and_lons(projection, 1d_proj_coors, crs_params): +def create_2d_lats_and_lons(projection, proj_1d_coors, crs_params): """TODO.""" # TODO functional code here to go from inputs to lat_data and lon_data diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 73eaee53fe..291f2f6375 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -102,11 +102,11 @@ def __init__( proj_id=None, # i.e. WGS1984_CF_ATTR_DEFAULTS.reference_ellipsoid_name: reference_ellipsoid_name="WGS 84", - semi_major_axis=WGS1984_CF_ATTR_DEFAULTS.semi_major_axis, - semi_minor_axis=WGS1984_CF_ATTR_DEFAULTS.semi_minor_axis, - inverse_flattening=WGS1984_CF_ATTR_DEFAULTS.inverse_flattening, - prime_meridian_name=WGS1984_CF_ATTR_DEFAULTS.prime_meridian_name, - longitude_of_prime_meridian=WGS1984_CF_ATTR_DEFAULTS.longitude_of_prime_meridian, + semi_major_axis=WGS1984_CF_ATTR_DEFAULTS["semi_major_axis"], + semi_minor_axis=WGS1984_CF_ATTR_DEFAULTS["semi_minor_axis"], + inverse_flattening=WGS1984_CF_ATTR_DEFAULTS["inverse_flattening"], + prime_meridian_name=WGS1984_CF_ATTR_DEFAULTS["prime_meridian_name"], + longitude_of_prime_meridian=WGS1984_CF_ATTR_DEFAULTS["longitude_of_prime_meridian"], earth_radius=None, ): """**Initialisation** @@ -214,6 +214,30 @@ def __init__( self.semi_major_axis = semi_major_axis self.semi_minor_axis = semi_minor_axis + def get_proj_string(self): + """TODO""" + # TODO finish to return parameters for full proj string + return f"+proj={self.proj_id}" + + def __repr__(self): + """x.__repr__() <==> repr(x)""" + # Report parent GridMapping class to indicate classification, + # but only if it has one (> 2 avoids own class and 'object') + # base. E.g. we get , + # , . + parent_gm = "" + if len(self.__class__.__mro__) > 2: + parent_gm = self.__class__.__mro__[1].__name__ + ": " + return ( + f"" + ) + + def __str__(self): + """x.__str__() <==> str(x)""" + return ( + f"{self.__repr__()[:-1]} {self.get_proj_string()}>" + ) + class AzimuthalGridMapping(GridMapping): """A Grid Mapping with Azimuthal classification. @@ -621,7 +645,7 @@ def __init__( # sweep_angle_axis must be the opposite (of "x" and "y") to # fixed_angle_axis. if (sweep_angle_axis.lower(), fixed_angle_axis.lower()) not in [ - ("x", "y")("y", "x") + ("x", "y"),("y", "x") ]: raise ValueError( "The sweep_angle_axis must be the opposite value, from 'x' " @@ -1212,7 +1236,7 @@ def __init__( **kwargs, ): super().__init__( - "rotated_latitude_longitude", "latlon", *args, **kwargs + "rotated_latitude_longitude", "latlong", *args, **kwargs ) self.grid_north_pole_latitude = grid_north_pole_latitude @@ -1236,7 +1260,7 @@ class LatitudeLongitude(LatLonGridMapping): """ def __init__(self, *args, **kwargs): - super().__init__("latitude_longitude", "latlon", *args, **kwargs) + super().__init__("latitude_longitude", "latlong", *args, **kwargs) class Sinusoidal(GridMapping): From 4b3ea11494d7279b14fe3a5b6edc294df70afb08 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Thu, 3 Aug 2023 12:23:33 +0100 Subject: [PATCH 20/97] Use abstract base class to improve classification of Grid Mappings --- cf/gridmappings.py | 371 ++++++++++++++++++++++++++++----------------- 1 file changed, 230 insertions(+), 141 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 291f2f6375..7c3bb8f412 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -1,5 +1,8 @@ +from abc import ABC, abstractmethod + from pyproj import CRS + ALL_GRID_MAPPING_ATTR_NAMES = { "grid_mapping_name", # *Those which describe the ellipsoid and prime meridian:* @@ -93,50 +96,26 @@ """ -class GridMapping: +class GridMapping(ABC): """A container for a Grid Mapping recognised by the CF Conventions.""" def __init__( self, - grid_mapping_name=None, - proj_id=None, # i.e. WGS1984_CF_ATTR_DEFAULTS.reference_ellipsoid_name: reference_ellipsoid_name="WGS 84", semi_major_axis=WGS1984_CF_ATTR_DEFAULTS["semi_major_axis"], - semi_minor_axis=WGS1984_CF_ATTR_DEFAULTS["semi_minor_axis"], + semi_minor_axis=WGS1984_CF_ATTR_DEFAULTS["semi_minor_axis"], inverse_flattening=WGS1984_CF_ATTR_DEFAULTS["inverse_flattening"], - prime_meridian_name=WGS1984_CF_ATTR_DEFAULTS["prime_meridian_name"], - longitude_of_prime_meridian=WGS1984_CF_ATTR_DEFAULTS["longitude_of_prime_meridian"], + prime_meridian_name=WGS1984_CF_ATTR_DEFAULTS["prime_meridian_name"], + longitude_of_prime_meridian=WGS1984_CF_ATTR_DEFAULTS[ + "longitude_of_prime_meridian" + ], earth_radius=None, ): """**Initialisation** :Parameters: - Parameters to define the grid mapping: - - grid_mapping_name: `str` - The value of the 'grid_mapping_name' attribute - attached to a data variable, for example - "mercator" to indicate the Mercator projection. - - proj_id: `str` - The PROJ projection identifier shorthand name that - corresponds to the specified 'grid_mapping_name' - attribute, for example "merc" for the Mercator - projection. This is the initial component in the - PROJ 'proj-string' to describe the coordinate - transformation. - - .. note:: Do not specify the full 'proj-string' - including parameters, since these are - calculated from the class input parameters, - and do not include the projection specifier - '+proj' as a prefix. Only give the - projection ID. - - Parameters to define the ellipsoid size and shape: - reference_ellipsoid_name: `str` or `None`, optional The name of a built-in ellipsoid definition. The default is "WGS 84". @@ -192,17 +171,6 @@ def __init__( takes precedence. """ - if not grid_mapping_name and not proj_id: - raise NotImplementedError( - "Must define a specific Grid Mapping via setting its CF " - "Conventions 'grid_mapping_name' attribute value with the " - "grid_mapping_name parameter, as well as the corresponding " - "base PROJ '+proj' identifier with the proj_id parameter." - ) - - # Defining the Grid Mapping - self.grid_mapping_name = grid_mapping_name - self.proj_id = proj_id # The attributes which describe the ellipsoid and prime meridian, # which may be included, when applicable, with any grid mapping @@ -214,6 +182,18 @@ def __init__( self.semi_major_axis = semi_major_axis self.semi_minor_axis = semi_minor_axis + @property + @abstractmethod + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + pass + + @property + @abstractmethod + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + pass + def get_proj_string(self): """TODO""" # TODO finish to return parameters for full proj string @@ -228,15 +208,11 @@ def __repr__(self): parent_gm = "" if len(self.__class__.__mro__) > 2: parent_gm = self.__class__.__mro__[1].__name__ + ": " - return ( - f"" - ) + return f"" def __str__(self): """x.__str__() <==> str(x)""" - return ( - f"{self.__repr__()[:-1]} {self.get_proj_string()}>" - ) + return f"{self.__repr__()[:-1]} {self.get_proj_string()}>" class AzimuthalGridMapping(GridMapping): @@ -489,17 +465,15 @@ class AlbersEqualArea(ConicGridMapping): """ - def __init__( - self, - standard_parallel, - longitude_of_central_meridian=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - *args, - **kwargs, - ): - super().__init__("albers_conical_equal_area", "aea", *args, **kwargs) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "albers_conical_equal_area" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "aea" class AzimuthalEquidistant(AzimuthalGridMapping): @@ -547,16 +521,15 @@ class AzimuthalEquidistant(AzimuthalGridMapping): """ - def __init__( - self, - longitude_of_projection_origin=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - *args, - **kwargs, - ): - super().__init__("azimuthal_equidistant", "aeqd", *args, **kwargs) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "azimuthal_equidistant" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "aeqd" class Geostationary(PerspectiveGridMapping): @@ -631,21 +604,30 @@ class Geostationary(PerspectiveGridMapping): def __init__( self, perspective_point_height, + *args, longitude_of_projection_origin=0.0, latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, sweep_angle_axis="y", fixed_angle_axis="x", - *args, **kwargs, ): - super().__init__("geostationary", "geos", *args, **kwargs) + super().__init__( + perspective_point_height, + *args, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, + false_easting=0.0, + false_northing=0.0, + **kwargs, + ) # sweep_angle_axis must be the opposite (of "x" and "y") to # fixed_angle_axis. if (sweep_angle_axis.lower(), fixed_angle_axis.lower()) not in [ - ("x", "y"),("y", "x") + ("x", "y"), + ("y", "x"), ]: raise ValueError( "The sweep_angle_axis must be the opposite value, from 'x' " @@ -656,6 +638,16 @@ def __init__( self.sweep_angle_axis = sweep_angle_axis.lower() self.fixed_angle_axis = fixed_angle_axis.lower() + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "geostationary" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "geos" + class LambertAzimuthalEqualArea(AzimuthalGridMapping): """The Lambert Azimuthal Equal Area grid mapping. @@ -702,18 +694,15 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): """ - def __init__( - self, - longitude_of_projection_origin=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - *args, - **kwargs, - ): - super().__init__( - "lambert_azimuthal_equal_area", "laea", *args, **kwargs - ) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "lambert_azimuthal_equal_area" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "laea" class LambertConformalConic(ConicGridMapping): @@ -775,17 +764,15 @@ class LambertConformalConic(ConicGridMapping): """ - def __init__( - self, - standard_parallel, - longitude_of_central_meridian=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - *args, - **kwargs, - ): - super().__init__("lambert_conformal_conic", "lcc", *args, **kwargs) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "lambert_conformal_conic" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "lcc" class LambertCylindricalEqualArea(CylindricalGridMapping): @@ -846,16 +833,16 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): def __init__( self, - standard_parallel=(0.0, None), + *args, false_easting=0.0, false_northing=0.0, + standard_parallel=(0.0, None), scale_factor_at_projection_origin=1.0, longitude_of_central_meridian=0.0, - *args, **kwargs, ): super().__init__( - "lambert_cylindrical_equal_area", "cea", *args, **kwargs + *args, false_easting=0.0, false_northing=0.0, **kwargs ) self.standard_parallel = standard_parallel @@ -864,6 +851,16 @@ def __init__( scale_factor_at_projection_origin ) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "lambert_cylindrical_equal_area" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "cea" + class Mercator(CylindricalGridMapping): """The Mercator grid mapping. @@ -923,15 +920,17 @@ class Mercator(CylindricalGridMapping): def __init__( self, - standard_parallel=(0.0, None), - longitude_of_projection_origin=0.0, + *args, false_easting=0.0, false_northing=0.0, + standard_parallel=(0.0, None), + longitude_of_projection_origin=0.0, scale_factor_at_projection_origin=1.0, - *args, **kwargs, ): - super().__init__("mercator", "merc", *args, **kwargs) + super().__init__( + *args, false_easting=0.0, false_northing=0.0, **kwargs + ) self.standard_parallel = standard_parallel self.longitude_of_projection_origin = longitude_of_projection_origin @@ -939,6 +938,16 @@ def __init__( scale_factor_at_projection_origin ) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "mercator" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "merc" + class ObliqueMercator(CylindricalGridMapping): """The Oblique Mercator grid mapping. @@ -1001,16 +1010,18 @@ class ObliqueMercator(CylindricalGridMapping): def __init__( self, + *args, 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, false_easting=0.0, false_northing=0.0, - scale_factor_at_projection_origin=1.0, - *args, **kwargs, ): - super().__init__("oblique_mercator", "omerc", *args, **kwargs) + super().__init__( + *args, false_easting=0.0, false_northing=0.0, **kwargs + ) self.azimuth_of_central_line = azimuth_of_central_line self.latitude_of_projection_origin = latitude_of_projection_origin @@ -1019,6 +1030,16 @@ def __init__( scale_factor_at_projection_origin ) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "oblique_mercator" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "omerc" + class Orthographic(AzimuthalGridMapping): """The Orthographic grid mapping. @@ -1065,16 +1086,15 @@ class Orthographic(AzimuthalGridMapping): """ - def __init__( - self, - longitude_of_projection_origin=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - *args, - **kwargs, - ): - super().__init__("orthographic", "ortho", *args, **kwargs) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "orthographic" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "ortho" class PolarStereographic(AzimuthalGridMapping): @@ -1151,19 +1171,26 @@ class PolarStereographic(AzimuthalGridMapping): def __init__( self, - standard_parallel=(0.0, 0.0), + *args, latitude_of_projection_origin=0.0, longitude_of_projection_origin=0.0, - straight_vertical_longitude_from_pole=0.0, false_easting=0.0, false_northing=0.0, + standard_parallel=(0.0, 0.0), + straight_vertical_longitude_from_pole=0.0, scale_factor_at_projection_origin=1.0, - *args, **kwargs, ): # TODO check defaults here, they do not appear for # CRS.from_proj4("+proj=ups").to_cf() to cross reference! - super().__init__("polar_stereographic", "ups", *args, **kwargs) + super().__init__( + *args, + latitude_of_projection_origin=0.0, + longitude_of_projection_origin=0.0, + false_easting=0.0, + false_northing=0.0, + **kwargs, + ) # See: https://github.com/cf-convention/cf-conventions/issues/445 if ( @@ -1183,6 +1210,16 @@ def __init__( scale_factor_at_projection_origin ) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "polar_stereographic" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "ups" + class RotatedLatitudeLongitude(LatLonGridMapping): """The Rotated Latitude-Longitude grid mapping. @@ -1231,18 +1268,26 @@ def __init__( self, grid_north_pole_latitude, grid_north_pole_longitude, - north_pole_grid_longitude=0.0, *args, + north_pole_grid_longitude=0.0, **kwargs, ): - super().__init__( - "rotated_latitude_longitude", "latlong", *args, **kwargs - ) + super().__init__(*args, **kwargs) self.grid_north_pole_latitude = grid_north_pole_latitude self.grid_north_pole_longitude = grid_north_pole_longitude self.north_pole_grid_longitude = north_pole_grid_longitude + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "rotated_latitude_longitude" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "latlong" + class LatitudeLongitude(LatLonGridMapping): """The Latitude-Longitude grid mapping. @@ -1259,8 +1304,15 @@ class LatitudeLongitude(LatLonGridMapping): """ - def __init__(self, *args, **kwargs): - super().__init__("latitude_longitude", "latlong", *args, **kwargs) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "latitude_longitude" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "latlong" class Sinusoidal(GridMapping): @@ -1302,18 +1354,28 @@ class Sinusoidal(GridMapping): def __init__( self, + *args, longitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, - *args, **kwargs, ): - super().__init__("sinusoidal", "sinu", *args, **kwargs) + super().__init__(*args, **kwargs) self.longitude_of_projection_origin = longitude_of_projection_origin self.false_easting = false_easting self.false_northing = false_northing + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "sinusoidal" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "sinu" + class Stereographic(AzimuthalGridMapping): """The Stereographic grid mapping. @@ -1366,20 +1428,37 @@ class Stereographic(AzimuthalGridMapping): def __init__( self, + *args, false_easting=0.0, false_northing=0.0, longitude_of_projection_origin=0.0, latitude_of_projection_origin=0.0, scale_factor_at_projection_origin=1.0, - *args, **kwargs, ): - super().__init__("stereographic", "stere", *args, **kwargs) + super().__init__( + *args, + false_easting=0.0, + false_northing=0.0, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, + **kwargs, + ) self.scale_factor_at_projection_origin = ( scale_factor_at_projection_origin ) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "stereographic" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "stere" + class TransverseMercator(CylindricalGridMapping): """The Transverse Mercator grid mapping. @@ -1432,15 +1511,17 @@ class TransverseMercator(CylindricalGridMapping): def __init__( self, + *args, scale_factor_at_central_meridian=1.0, longitude_of_central_meridian=0.0, latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, - *args, **kwargs, ): - super().__init__("transverse_mercator", "tmerc", *args, **kwargs) + super().__init__( + *args, false_easting=0.0, false_northing=0.0, **kwargs + ) self.scale_factor_at_central_meridian = ( scale_factor_at_central_meridian @@ -1448,6 +1529,16 @@ def __init__( self.longitude_of_central_meridian = longitude_of_central_meridian self.latitude_of_projection_origin = latitude_of_projection_origin + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "transverse_mercator" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "tmerc" + class VerticalPerspective(PerspectiveGridMapping): """The Vertical (or Near-sided) Perspective grid mapping. @@ -1499,14 +1590,12 @@ class VerticalPerspective(PerspectiveGridMapping): """ - def __init__( - self, - perspective_point_height, - longitude_of_projection_origin=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - *args, - **kwargs, - ): - super().__init__("vertical_perspective", "nsper", *args, **kwargs) + @property + def grid_mapping_name(self): + """The value of the 'grid_mapping_name' attribute.""" + return "vertical_perspective" + + @property + def proj_id(self): + """The PROJ projection identifier shorthand name.""" + return "nsper" From b684ffb0cbe86c18dad5ad387d5bddeaa56ce956 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Thu, 3 Aug 2023 17:07:17 +0100 Subject: [PATCH 21/97] Tidy Grid Mapping class inehritance --- cf/gridmappings.py | 64 ++++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 7c3bb8f412..b8f8ecee3c 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -101,15 +101,15 @@ class GridMapping(ABC): def __init__( self, - # i.e. WGS1984_CF_ATTR_DEFAULTS.reference_ellipsoid_name: + # i.e. WGS1984_CF_ATTR_DEFAULTS["reference_ellipsoid_name"], etc. reference_ellipsoid_name="WGS 84", + # The next three parameters are non-zero floats so don't hard-code + # WGS84 defaults in case of future precision changes: semi_major_axis=WGS1984_CF_ATTR_DEFAULTS["semi_major_axis"], semi_minor_axis=WGS1984_CF_ATTR_DEFAULTS["semi_minor_axis"], inverse_flattening=WGS1984_CF_ATTR_DEFAULTS["inverse_flattening"], - prime_meridian_name=WGS1984_CF_ATTR_DEFAULTS["prime_meridian_name"], - longitude_of_prime_meridian=WGS1984_CF_ATTR_DEFAULTS[ - "longitude_of_prime_meridian" - ], + prime_meridian_name="Greenwich", + longitude_of_prime_meridian=0.0, earth_radius=None, ): """**Initialisation** @@ -194,10 +194,11 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" pass + @property + @abstractmethod def get_proj_string(self): - """TODO""" - # TODO finish to return parameters for full proj string - return f"+proj={self.proj_id}" + """The value of the PROJ proj-string defining the projection.""" + pass def __repr__(self): """x.__repr__() <==> repr(x)""" @@ -254,10 +255,9 @@ def __init__( latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, - *args, **kwargs, ): - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.longitude_of_projection_origin = longitude_of_projection_origin self.latitude_of_projection_origin = latitude_of_projection_origin @@ -319,10 +319,9 @@ def __init__( latitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, - *args, **kwargs, ): - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.standard_parallel = standard_parallel self.longitude_of_central_meridian = longitude_of_central_meridian @@ -348,8 +347,8 @@ class CylindricalGridMapping(GridMapping): """ - def __init__(self, false_easting=0.0, false_northing=0.0, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, false_easting=0.0, false_northing=0.0, **kwargs): + super().__init__(**kwargs) self.false_easting = false_easting self.false_northing = false_northing @@ -365,9 +364,7 @@ class LatLonGridMapping(GridMapping): .. versionadded:: GMVER """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + pass class PerspectiveGridMapping(AzimuthalGridMapping): @@ -384,13 +381,8 @@ class PerspectiveGridMapping(AzimuthalGridMapping): """ - def __init__( - self, - perspective_point_height, - *args, - **kwargs, - ): - super().__init__(*args, **kwargs) + def __init__(self, perspective_point_height, **kwargs): + super().__init__(**kwargs) self.perspective_point_height = perspective_point_height @@ -604,7 +596,6 @@ class Geostationary(PerspectiveGridMapping): def __init__( self, perspective_point_height, - *args, longitude_of_projection_origin=0.0, latitude_of_projection_origin=0.0, false_easting=0.0, @@ -615,7 +606,6 @@ def __init__( ): super().__init__( perspective_point_height, - *args, longitude_of_projection_origin=0.0, latitude_of_projection_origin=0.0, false_easting=0.0, @@ -833,7 +823,6 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): def __init__( self, - *args, false_easting=0.0, false_northing=0.0, standard_parallel=(0.0, None), @@ -842,7 +831,7 @@ def __init__( **kwargs, ): super().__init__( - *args, false_easting=0.0, false_northing=0.0, **kwargs + false_easting=0.0, false_northing=0.0, **kwargs ) self.standard_parallel = standard_parallel @@ -920,7 +909,6 @@ class Mercator(CylindricalGridMapping): def __init__( self, - *args, false_easting=0.0, false_northing=0.0, standard_parallel=(0.0, None), @@ -929,7 +917,7 @@ def __init__( **kwargs, ): super().__init__( - *args, false_easting=0.0, false_northing=0.0, **kwargs + false_easting=0.0, false_northing=0.0, **kwargs ) self.standard_parallel = standard_parallel @@ -1010,7 +998,6 @@ class ObliqueMercator(CylindricalGridMapping): def __init__( self, - *args, azimuth_of_central_line=0.0, latitude_of_projection_origin=0.0, longitude_of_projection_origin=0.0, @@ -1020,7 +1007,7 @@ def __init__( **kwargs, ): super().__init__( - *args, false_easting=0.0, false_northing=0.0, **kwargs + false_easting=0.0, false_northing=0.0, **kwargs ) self.azimuth_of_central_line = azimuth_of_central_line @@ -1171,7 +1158,6 @@ class PolarStereographic(AzimuthalGridMapping): def __init__( self, - *args, latitude_of_projection_origin=0.0, longitude_of_projection_origin=0.0, false_easting=0.0, @@ -1184,7 +1170,6 @@ def __init__( # TODO check defaults here, they do not appear for # CRS.from_proj4("+proj=ups").to_cf() to cross reference! super().__init__( - *args, latitude_of_projection_origin=0.0, longitude_of_projection_origin=0.0, false_easting=0.0, @@ -1268,11 +1253,10 @@ def __init__( self, grid_north_pole_latitude, grid_north_pole_longitude, - *args, north_pole_grid_longitude=0.0, **kwargs, ): - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.grid_north_pole_latitude = grid_north_pole_latitude self.grid_north_pole_longitude = grid_north_pole_longitude @@ -1354,13 +1338,12 @@ class Sinusoidal(GridMapping): def __init__( self, - *args, longitude_of_projection_origin=0.0, false_easting=0.0, false_northing=0.0, **kwargs, ): - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.longitude_of_projection_origin = longitude_of_projection_origin self.false_easting = false_easting @@ -1428,7 +1411,6 @@ class Stereographic(AzimuthalGridMapping): def __init__( self, - *args, false_easting=0.0, false_northing=0.0, longitude_of_projection_origin=0.0, @@ -1437,7 +1419,6 @@ def __init__( **kwargs, ): super().__init__( - *args, false_easting=0.0, false_northing=0.0, longitude_of_projection_origin=0.0, @@ -1511,7 +1492,6 @@ class TransverseMercator(CylindricalGridMapping): def __init__( self, - *args, scale_factor_at_central_meridian=1.0, longitude_of_central_meridian=0.0, latitude_of_projection_origin=0.0, @@ -1520,7 +1500,7 @@ def __init__( **kwargs, ): super().__init__( - *args, false_easting=0.0, false_northing=0.0, **kwargs + false_easting=0.0, false_northing=0.0, **kwargs ) self.scale_factor_at_central_meridian = ( From 37a900eae6bea29b534a3b3e9deceb1a0ab02e20 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Fri, 4 Aug 2023 15:07:58 +0100 Subject: [PATCH 22/97] Grid Mappings: add method to return PROJ proj-string --- cf/gridmappings.py | 96 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index b8f8ecee3c..a21caf01bb 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -3,6 +3,7 @@ from pyproj import CRS +PROJ_PREFIX="+proj" ALL_GRID_MAPPING_ATTR_NAMES = { "grid_mapping_name", # *Those which describe the ellipsoid and prime meridian:* @@ -46,6 +47,26 @@ "projected_crs_name", "towgs84", # ------------------------- PROJ '+towgs84' value } +GRID_MAPPING_ATTR_TO_PROJ_STRING_COMP = { + "earth_radius": "R", + "inverse_flattening": "rf", + "prime_meridian_name": "pm", + "reference_ellipsoid_name": "ellps", + "semi_major_axis": "a", + "semi_minor_axis": "b", + "longitude_of_projection_origin": "lon_0", + "latitude_of_projection_origin": "lat_0", + "scale_factor_at_projection_origin": "k_0", + "false_easting": "x_0", + "false_northing": "y_0", + "sweep_angle_axis": "sweep", + "standard_parallel": ("lat_1", "lat_2"), + "perspective_point_height": "h", + "azimuth_of_central_line": "alpha", + "crs_wkt": "crs_wkt", + "towgs84": "towgs84", +} + """ Define this first since it provides the default for several parameters, @@ -111,6 +132,7 @@ def __init__( prime_meridian_name="Greenwich", longitude_of_prime_meridian=0.0, earth_radius=None, + **kwargs, ): """**Initialisation** @@ -172,8 +194,15 @@ def __init__( """ + for kwarg in kwargs: + if kwarg not in ALL_GRID_MAPPING_ATTR_NAMES: + raise ValueError( + "Unrecognised map parameter provided for the " + f"Grid Mapping: {kwarg}" + ) + # The attributes which describe the ellipsoid and prime meridian, - # which may be included, when applicable, with any grid mapping + # which may be included, when applicable, with any grid mapping. self.earth_radius = earth_radius self.inverse_flattening = inverse_flattening self.longitude_of_prime_meridian = longitude_of_prime_meridian @@ -194,7 +223,6 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" pass - @property @abstractmethod def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" @@ -467,6 +495,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "aea" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class AzimuthalEquidistant(AzimuthalGridMapping): """The Azimuthal Equidistant grid mapping. @@ -523,6 +555,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "aeqd" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class Geostationary(PerspectiveGridMapping): """The Geostationary Satellite View grid mapping. @@ -638,6 +674,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "geos" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class LambertAzimuthalEqualArea(AzimuthalGridMapping): """The Lambert Azimuthal Equal Area grid mapping. @@ -694,6 +734,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "laea" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class LambertConformalConic(ConicGridMapping): """The Lambert Conformal Conic grid mapping. @@ -764,6 +808,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "lcc" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class LambertCylindricalEqualArea(CylindricalGridMapping): """The Equal Area Cylindrical grid mapping. @@ -850,6 +898,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "cea" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class Mercator(CylindricalGridMapping): """The Mercator grid mapping. @@ -936,6 +988,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "merc" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class ObliqueMercator(CylindricalGridMapping): """The Oblique Mercator grid mapping. @@ -1027,6 +1083,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "omerc" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class Orthographic(AzimuthalGridMapping): """The Orthographic grid mapping. @@ -1083,6 +1143,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "ortho" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class PolarStereographic(AzimuthalGridMapping): """The Universal Polar Stereographic grid mapping. @@ -1205,6 +1269,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "ups" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class RotatedLatitudeLongitude(LatLonGridMapping): """The Rotated Latitude-Longitude grid mapping. @@ -1272,6 +1340,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "latlong" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class LatitudeLongitude(LatLonGridMapping): """The Latitude-Longitude grid mapping. @@ -1298,6 +1370,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "latlong" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class Sinusoidal(GridMapping): """The Sinusoidal (Sanson-Flamsteed) grid mapping. @@ -1359,6 +1435,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "sinu" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class Stereographic(AzimuthalGridMapping): """The Stereographic grid mapping. @@ -1440,6 +1520,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "stere" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class TransverseMercator(CylindricalGridMapping): """The Transverse Mercator grid mapping. @@ -1519,6 +1603,10 @@ def proj_id(self): """The PROJ projection identifier shorthand name.""" return "tmerc" + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + class VerticalPerspective(PerspectiveGridMapping): """The Vertical (or Near-sided) Perspective grid mapping. @@ -1579,3 +1667,7 @@ def grid_mapping_name(self): def proj_id(self): """The PROJ projection identifier shorthand name.""" return "nsper" + + def get_proj_string(self): + """The value of the PROJ proj-string defining the projection.""" + return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" From e571d59c846ac9c60b30170cd2648a166005ad0b Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Fri, 4 Aug 2023 17:41:45 +0100 Subject: [PATCH 23/97] Create internal helper functions for unit conversion etc. --- cf/gridmappings.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index a21caf01bb..50301ee4c6 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -99,6 +99,33 @@ WGS1984_CF_ATTR_DEFAULTS = CRS.from_proj4("+proj=merc").to_cf() +def _convert_units_cf_to_proj(cf_units): + """Take CF units and convert them to equivalent units under PROJ.""" + pass + + +def _convert_units_proj_to_cf(cf_units): + """Take units used in PROJ and convert them to CF units.""" + pass + + +def _make_proj_string_comp(spec): + """Form a PROJ proj-string end from the given PROJ parameters. + + :Parameters: + + spec: `dict` + A dictionary providing the proj-string specifiers for + parameters, as keys, with their values as values. Values + must be convertible to strings. + + """ + proj_string = "" + for comp, value in spec.items(): + proj_string += f" +{comp}={value}" + return proj_string + + """Abstract classes for general Grid Mappings. Note that default arguments are based upon the PROJ defaults, which can @@ -148,8 +175,8 @@ def __init__( inverse_flattening: number, optional The reverse flattening of the ellipsoid (PROJ 'rf' value), :math:`\frac{1}{f}`, where f corresponds to - the flattening value (PROJ 'f' value) for the ellipsoid. - Unitless. The default is 298.257223563. + the flattening value (PROJ 'f' value) for the + ellipsoid. Unitless. The default is 298.257223563. prime_meridian_name: `str`, optional A predeclared name to define the prime meridian (PROJ From e379c5226f4ea35a2eb5bf9d4976ff42adf3dac8 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 7 Aug 2023 10:42:21 +0100 Subject: [PATCH 24/97] Grid Mappings: add proj-string parameter creator function --- cf/gridmappings.py | 78 ++++++++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 50301ee4c6..f0209225fe 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -2,8 +2,7 @@ from pyproj import CRS - -PROJ_PREFIX="+proj" +PROJ_PREFIX = "+proj" ALL_GRID_MAPPING_ATTR_NAMES = { "grid_mapping_name", # *Those which describe the ellipsoid and prime meridian:* @@ -98,6 +97,8 @@ """ WGS1984_CF_ATTR_DEFAULTS = CRS.from_proj4("+proj=merc").to_cf() +DUMMY_PARAMS = {"a": "b", "c": 0.0} # TODOPARAMETERS, drop this + def _convert_units_cf_to_proj(cf_units): """Take CF units and convert them to equivalent units under PROJ.""" @@ -122,6 +123,14 @@ def _make_proj_string_comp(spec): """ proj_string = "" for comp, value in spec.items(): + if not isinstance(value, str): + try: + value = str(value) + except TypeError: + raise TypeError( + "Can't create proj-string due to non-representable " + f"value {value} for key {comp}" + ) proj_string += f" +{comp}={value}" return proj_string @@ -419,6 +428,7 @@ class LatLonGridMapping(GridMapping): .. versionadded:: GMVER """ + pass @@ -524,7 +534,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class AzimuthalEquidistant(AzimuthalGridMapping): @@ -584,7 +595,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class Geostationary(PerspectiveGridMapping): @@ -703,7 +715,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class LambertAzimuthalEqualArea(AzimuthalGridMapping): @@ -763,7 +776,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class LambertConformalConic(ConicGridMapping): @@ -837,7 +851,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class LambertCylindricalEqualArea(CylindricalGridMapping): @@ -905,9 +920,7 @@ def __init__( longitude_of_central_meridian=0.0, **kwargs, ): - super().__init__( - false_easting=0.0, false_northing=0.0, **kwargs - ) + super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) self.standard_parallel = standard_parallel self.longitude_of_central_meridian = longitude_of_central_meridian @@ -927,7 +940,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class Mercator(CylindricalGridMapping): @@ -995,9 +1009,7 @@ def __init__( scale_factor_at_projection_origin=1.0, **kwargs, ): - super().__init__( - false_easting=0.0, false_northing=0.0, **kwargs - ) + super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) self.standard_parallel = standard_parallel self.longitude_of_projection_origin = longitude_of_projection_origin @@ -1017,7 +1029,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class ObliqueMercator(CylindricalGridMapping): @@ -1089,9 +1102,7 @@ def __init__( false_northing=0.0, **kwargs, ): - super().__init__( - false_easting=0.0, false_northing=0.0, **kwargs - ) + super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) self.azimuth_of_central_line = azimuth_of_central_line self.latitude_of_projection_origin = latitude_of_projection_origin @@ -1112,7 +1123,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class Orthographic(AzimuthalGridMapping): @@ -1172,7 +1184,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class PolarStereographic(AzimuthalGridMapping): @@ -1298,7 +1311,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class RotatedLatitudeLongitude(LatLonGridMapping): @@ -1369,7 +1383,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class LatitudeLongitude(LatLonGridMapping): @@ -1399,7 +1414,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class Sinusoidal(GridMapping): @@ -1464,7 +1480,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class Stereographic(AzimuthalGridMapping): @@ -1549,7 +1566,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class TransverseMercator(CylindricalGridMapping): @@ -1610,9 +1628,7 @@ def __init__( false_northing=0.0, **kwargs, ): - super().__init__( - false_easting=0.0, false_northing=0.0, **kwargs - ) + super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) self.scale_factor_at_central_meridian = ( scale_factor_at_central_meridian @@ -1632,7 +1648,8 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" class VerticalPerspective(PerspectiveGridMapping): @@ -1697,4 +1714,5 @@ def proj_id(self): def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" - return f"{PROJ_PREFIX}={self.proj_id} TODOPARAMETERS" + parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS + return f"{PROJ_PREFIX}={self.proj_id}{parameters}" From 84950b0eff9c795490e21dc5d41646c44cf9adf7 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 7 Aug 2023 14:10:10 +0100 Subject: [PATCH 25/97] Testing: add minimal new test module test_gridmappings.py --- cf/gridmappings.py | 6 +-- cf/test/test_gridmappings.py | 82 ++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 cf/test/test_gridmappings.py diff --git a/cf/gridmappings.py b/cf/gridmappings.py index f0209225fe..053ac23063 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -1336,8 +1336,7 @@ class RotatedLatitudeLongitude(LatLonGridMapping): units of decimal degrees, where forming a string by adding a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. + of 'd', 'D' or '°' confirm units of decimal degrees. grid_north_pole_longitude: number or `str` Longitude of the North pole of the unrotated source CRS, @@ -1345,8 +1344,7 @@ class RotatedLatitudeLongitude(LatLonGridMapping): units of decimal degrees, where forming a string by adding a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. + of 'd', 'D' or '°' confirm units of decimal degrees. north_pole_grid_longitude: number or `str`, optional The longitude of projection center (PROJ 'lon_0' value), in diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py new file mode 100644 index 0000000000..76b78f0f43 --- /dev/null +++ b/cf/test/test_gridmappings.py @@ -0,0 +1,82 @@ +import datetime +import faulthandler +import unittest + +# import numpy as np + +faulthandler.enable() # to debug seg faults and timeouts + +import cf + +pyproj_imported = False +try: + import pyproj + + pyproj_imported = True +except ImportError: + pass + + +all_abstract_grid_mappings = ( + cf.GridMapping, + cf.AzimuthalGridMapping, + cf.ConicGridMapping, + cf.CylindricalGridMapping, + cf.LatLonGridMapping, + cf.PerspectiveGridMapping, +) +# Representing all Grid Mappings repsented by the CF Conventions (APpendix F) +all_concrete_grid_mappings = ( + cf.AlbersEqualArea, + cf.AzimuthalEquidistant, + cf.Geostationary, + cf.LambertAzimuthalEqualArea, + cf.LambertConformalConic, + cf.LambertCylindricalEqualArea, + cf.Mercator, + cf.ObliqueMercator, + cf.Orthographic, + cf.PolarStereographic, + cf.RotatedLatitudeLongitude, + cf.LatitudeLongitude, + cf.Sinusoidal, + cf.Stereographic, + cf.TransverseMercator, + cf.VerticalPerspective, +) +# These are those of the above which have required positional arguments +all_concrete_grid_mappings_req_args = { + "AlbersEqualArea": {"standard_parallel": 0.0}, + "Geostationary": {"perspective_point_height": 1000}, + "VerticalPerspective": {"perspective_point_height": 1000}, + "LambertConformalConic": {"standard_parallel": 0.0}, + "RotatedLatitudeLongitude": { + "grid_north_pole_latitude": 0.0, + "grid_north_pole_longitude": 0.0, + }, +} + + +class GridMappingsTest(unittest.TestCase): + f = cf.example_field(1) + + @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") + def test_grid_mapping__repr__str__(self): + """TODO.""" + for cls in all_concrete_grid_mappings: + if cls.__name__ not in all_concrete_grid_mappings_req_args: + g = cls() + else: + example_minimal_args = all_concrete_grid_mappings_req_args[ + cls.__name__ + ] + g = cls(*example_minimal_args) + repr(g) + str(g) + + +if __name__ == "__main__": + print("Run date:", datetime.datetime.now()) + cf.environment() + print() + unittest.main(verbosity=2) From 9faa6fe5f68b1b278c0953d79b55312c052e39ea Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 8 Aug 2023 16:49:06 +0100 Subject: [PATCH 26/97] Add to test_gridmappings.py to prepare for GM matching --- cf/test/test_gridmappings.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 76b78f0f43..83ff1597c3 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -58,9 +58,19 @@ class GridMappingsTest(unittest.TestCase): - f = cf.example_field(1) + """TODO.""" - @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") + # Of the example fields, only 1, 6 and 7 have any coordinate references + # with a coordinate conversion, hence use these to test, plus 0 as an + # example case of a field without a coordinate reference at all. + f = cf.example_fields() + f0 = f[0] # No coordinate reference + f1 = f[1] # 2, with grid mappings of [None, 'rotated_latitude_longitude'] + f6 = f[6] # 1, with grid mapping of ['latitude_longitude'] + f7 = f[7] # 1, with grid mapping of ['rotated_latitude_longitude'] + f_with_gm = (f1, f6, f7) + + # @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping__repr__str__(self): """TODO.""" for cls in all_concrete_grid_mappings: @@ -74,6 +84,16 @@ def test_grid_mapping__repr__str__(self): repr(g) str(g) + def test_grid_mapping_find_gm_class(self): + """TODO.""" + for f in self.f_with_gm: + crefs = f.coordinate_references().values() + for cref in crefs: + gm = cref.coordinate_conversion.get_parameter( + "grid_mapping_name", default=None + ) + # TODO test that matches with GM class + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) From 5ce54202edbfa9a21bbf279164aaa3ffe32dcfbe Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 8 Aug 2023 18:37:45 +0100 Subject: [PATCH 27/97] Set-up grid_mapping_name attribute value to GM class matching --- cf/coordinateconversion.py | 5 +++++ cf/test/test_gridmappings.py | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cf/coordinateconversion.py b/cf/coordinateconversion.py index f2a4650138..09bdecf928 100644 --- a/cf/coordinateconversion.py +++ b/cf/coordinateconversion.py @@ -1,5 +1,7 @@ import cfdm +from .gridmappings import * # noqa: F403 + class CoordinateConversion(cfdm.CoordinateConversion): """A coordinate conversion component of a coordinate reference @@ -27,3 +29,6 @@ def __repr__(self): """ return super().__repr__().replace("<", " Date: Wed, 9 Aug 2023 12:47:37 +0100 Subject: [PATCH 28/97] Add equality checking for Grid Mapping classes --- cf/gridmappings.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 053ac23063..b08489da09 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -247,6 +247,25 @@ def __init__( self.semi_major_axis = semi_major_axis self.semi_minor_axis = semi_minor_axis + def __repr__(self): + """x.__repr__() <==> repr(x)""" + # Report parent GridMapping class to indicate classification, + # but only if it has one (> 2 avoids own class and 'object') + # base. E.g. we get , + # , . + parent_gm = "" + if len(self.__class__.__mro__) > 2: + parent_gm = self.__class__.__mro__[1].__name__ + ": " + return f"" + + def __str__(self): + """x.__str__() <==> str(x)""" + return f"{self.__repr__()[:-1]} {self.get_proj_string()}>" + + def __eq__(self, other): + """The rich comparison operator ``==``.""" + return self.get_proj_string() == other.get_proj_string() + @property @abstractmethod def grid_mapping_name(self): @@ -264,21 +283,6 @@ def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" pass - def __repr__(self): - """x.__repr__() <==> repr(x)""" - # Report parent GridMapping class to indicate classification, - # but only if it has one (> 2 avoids own class and 'object') - # base. E.g. we get , - # , . - parent_gm = "" - if len(self.__class__.__mro__) > 2: - parent_gm = self.__class__.__mro__[1].__name__ + ": " - return f"" - - def __str__(self): - """x.__str__() <==> str(x)""" - return f"{self.__repr__()[:-1]} {self.get_proj_string()}>" - class AzimuthalGridMapping(GridMapping): """A Grid Mapping with Azimuthal classification. From 5c1c7ffeb1dda726c4ccd8d38b4621f88f3964be Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Wed, 9 Aug 2023 17:44:41 +0100 Subject: [PATCH 29/97] Grid Mappings: add coordinate references for testing --- cf/test/test_gridmappings.py | 58 +++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index d03e20e7cd..8ffe449270 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -68,16 +68,54 @@ class GridMappingsTest(unittest.TestCase): f1 = f[1] # 2, with grid mappings of [None, 'rotated_latitude_longitude'] f6 = f[6] # 1, with grid mapping of ['latitude_longitude'] f7 = f[7] # 1, with grid mapping of ['rotated_latitude_longitude'] - f_with_gm = (f1, f6, f7) - gm_of_ex_fields = ( - None, - cf.RotatedLatitudeLongitude, - None, - None, - None, - None, - cf.LatitudeLongitude, - cf.RotatedLatitudeLongitude, + f_with_gm = { + f1: cf.RotatedLatitudeLongitude, + f6: cf.LatitudeLongitude, + f7: cf.RotatedLatitudeLongitude, + ) + + # From a custom netCDF file with Oblique Mercator GM + # TODO generate this .nc via create_test_files.py and un-commit + # forced commit of the (data-free / header-only) netCDF file. + f_om = cf.read("oblique_mercator.nc") + + # Create some coordinate references with different GMs to test on: + cr_aea = cf.CoordinateReference( + coordinates=["coordA", "coordB", "coordC"], + coordinate_conversion=cf.CoordinateConversion( + parameters={ + "grid_mapping_name": "albers_conical_equal_area", + "standard_parallel": [10, 10], + "longitude_of_projection_origin":45.0, + "false_easting": -1000, + "false_northing": 500, + } + ), + ) + cr_aea_actual_proj_string = ( + "+proj=aea +lat_1=10. +lat_2=10. +lon_0=45.0 +x_0=-1000. +y_0=-500." + ) + + cr_om = cf.CoordinateReference( + coordinates=["coordA", "coordB"], + coordinate_conversion=cf.CoordinateConversion( + parameters={ + "grid_mapping_name": "oblique_mercator", + "latitude_of_projection_origin": -22.0, + "longitude_of_projection_origin": -59.0, + "false_easting": -12500.0, + "false_northing": -12500.0, + "azimuth_of_central_line": 89.999999, + "scale_factor_at_projection_origin": 1.0, + "inverse_flattening": 0.0, + "semi_major_axis": 6371229.0, + } + ), + ) + cr_om_actual_proj_string = ( + "+proj=omerc +lat_0=-22.00 +alpha=89.999999 +lonc=-59.00 " + "+x_0=-12500. +y_0=-12500. +ellps=sphere +a=6371229. +b=6371229. " + "+units=m +no_defs" ) # @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") From 751c650751a620faa7b25f495e194c15be85b3d9 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Thu, 10 Aug 2023 13:57:33 +0100 Subject: [PATCH 30/97] Add hash for Grid Mappings classes for immutability --- cf/__init__.py | 2 ++ cf/coordinateconversion.py | 2 +- cf/gridmappings.py | 36 ++++++++++++++++++++++++++++++++ cf/test/test_gridmappings.py | 40 +++++++----------------------------- 4 files changed, 46 insertions(+), 34 deletions(-) diff --git a/cf/__init__.py b/cf/__init__.py index 6a85a9de62..5c9cbfea0d 100644 --- a/cf/__init__.py +++ b/cf/__init__.py @@ -346,6 +346,8 @@ Stereographic, TransverseMercator, VerticalPerspective, + _all_abstract_grid_mappings, + _all_concrete_grid_mappings, ) diff --git a/cf/coordinateconversion.py b/cf/coordinateconversion.py index 09bdecf928..608956fec1 100644 --- a/cf/coordinateconversion.py +++ b/cf/coordinateconversion.py @@ -30,5 +30,5 @@ def __repr__(self): """ return super().__repr__().replace("<", " Date: Thu, 10 Aug 2023 16:21:28 +0100 Subject: [PATCH 31/97] New method get_grid_mappings() to faciliate CC parameter access --- cf/coordinateconversion.py | 3 --- cf/domain.py | 30 ++++++++++++++++++++++++++++++ cf/field.py | 23 +++++++++++++++++++++++ cf/test/test_Domain.py | 7 +++++++ cf/test/test_Field.py | 13 +++++++++++++ cf/test/test_gridmappings.py | 9 +++------ 6 files changed, 76 insertions(+), 9 deletions(-) diff --git a/cf/coordinateconversion.py b/cf/coordinateconversion.py index 608956fec1..b3380d31e4 100644 --- a/cf/coordinateconversion.py +++ b/cf/coordinateconversion.py @@ -29,6 +29,3 @@ def __repr__(self): """ return super().__repr__().replace("<", ">> f.get_grid_mappings() + {"coordinatereference1": "rotated_latitude_longitude"} + >>> g.get_grid_mappings() + {} + + """ + gms = {} + for cref_name, cref in self.coordinate_references().items(): + gm = cref.coordinate_conversion.get_parameter( + "grid_mapping_name", default=None + ) + if gm: + gms[cref_name] = gm + return gms + def identity(self, default="", strict=False, relaxed=False, nc_only=False): """Return the canonical identity. diff --git a/cf/field.py b/cf/field.py index b840edeea7..912415e3c8 100644 --- a/cf/field.py +++ b/cf/field.py @@ -15642,3 +15642,26 @@ def unlimited(self, *args): version="3.0.0", removed_at="4.0.0", ) # pragma: no cover + + def get_grid_mappings(self): + """Returns coordinate conversions with grid mapping parameters. + + .. versionadded:: GMVER + + :Returns: + + `dict` + CoordinateConversion construct identifiers with + vaules of their 'grid_mapping_name' attributes, + for all CoordinateConversions of the domain that + have a 'grid_mapping_name' parameter defined. + + **Examples** + + >>> f.get_grid_mappings() + {"coordinatereference1": "rotated_latitude_longitude"} + >>> g.get_grid_mappings() + {} + + """ + return self.domain.get_grid_mappings() diff --git a/cf/test/test_Domain.py b/cf/test/test_Domain.py index 4f9d4fc96c..1fb5ee6b65 100644 --- a/cf/test/test_Domain.py +++ b/cf/test/test_Domain.py @@ -295,6 +295,13 @@ def test_Domain_transpose(self): def test_Domain_size(self): self.assertEqual(self.d.size, 90) + def test_Domain_get_grid_mappings(self): + self.assertEqual( + self.d.get_grid_mappings(), { + 'coordinatereference1': 'rotated_latitude_longitude' + } + ) + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/cf/test/test_Field.py b/cf/test/test_Field.py index 231fd35b6e..e210af48f2 100644 --- a/cf/test/test_Field.py +++ b/cf/test/test_Field.py @@ -2614,6 +2614,19 @@ def test_Field_auxiliary_to_dimension_to_auxiliary(self): with self.assertRaises(ValueError): f.auxiliary_to_dimension("latitude") + def test_Field_get_grid_mappings(self): + self.assertEqual( + self.f.get_grid_mappings(), + {'coordinatereference1': 'rotated_latitude_longitude'} + ) + self.assertEqual( + self.f0.get_grid_mappings(), {} + ) + self.assertEqual( + self.f1.get_grid_mappings(), + {'coordinatereference1': 'rotated_latitude_longitude'} + ) + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 2baeb7a96a..587c88377e 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -109,12 +109,9 @@ def test_grid_mapping__repr__str__(self): def test_grid_mapping_find_gm_class(self): """TODO.""" for f in self.f_with_gm: - crefs = f.coordinate_references().values() - for cref in crefs: - gm = cref.coordinate_conversion.get_parameter( - "grid_mapping_name", default=None - ) - # TODO test that matches with GM class + gms = f.domain.get_grid_mappings() + print(gms) + # TODO test that matches with GM class if __name__ == "__main__": From 19856c86c2602e69725f6381f3161f3baebba6ef Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 11 Aug 2023 15:22:36 +0100 Subject: [PATCH 32/97] Convert gm name property to class attribute --- cf/__init__.py | 1 + cf/domain.py | 21 ++++-- cf/gridmappings.py | 126 ++++++++++++----------------------- cf/test/test_gridmappings.py | 18 +++-- 4 files changed, 73 insertions(+), 93 deletions(-) diff --git a/cf/__init__.py b/cf/__init__.py index 5c9cbfea0d..8d27d48a21 100644 --- a/cf/__init__.py +++ b/cf/__init__.py @@ -348,6 +348,7 @@ VerticalPerspective, _all_abstract_grid_mappings, _all_concrete_grid_mappings, + _get_cf_grid_mapping_from_name, ) diff --git a/cf/domain.py b/cf/domain.py index 63e4b27f43..58373cf920 100644 --- a/cf/domain.py +++ b/cf/domain.py @@ -476,6 +476,17 @@ def get_data(self, default=ValueError(), _units=None, _fill_value=True): default, message=f"{self.__class__.__name__} has no data" ) + +def _get_cf_grid_mapping_from_name(gm_name): + """TODOSADIE.""" + cf_supported_gm_names = { + gm.grid_mapping_name: gm for gm in _all_concrete_grid_mappings + } + if gm_name in cf_supported_gm_names: + return cf_supported_gm_names[gm_name] + else: + return + def get_grid_mappings(self): """Returns coordinate conversions with grid mapping parameters. @@ -499,11 +510,11 @@ def get_grid_mappings(self): """ gms = {} for cref_name, cref in self.coordinate_references().items(): - gm = cref.coordinate_conversion.get_parameter( - "grid_mapping_name", default=None - ) - if gm: - gms[cref_name] = gm + gm = cref.coordinate_conversion.get_parameter( + "grid_mapping_name", default=None + ) + if gm: + gms[cref_name] = gm return gms def identity(self, default="", strict=False, relaxed=False, nc_only=False): diff --git a/cf/gridmappings.py b/cf/gridmappings.py index a7a17c576d..23f5676251 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -2,7 +2,6 @@ from pyproj import CRS - PROJ_PREFIX = "+proj" ALL_GRID_MAPPING_ATTR_NAMES = { "grid_mapping_name", @@ -230,7 +229,6 @@ def __init__( takes precedence. """ - for kwarg in kwargs: if kwarg not in ALL_GRID_MAPPING_ATTR_NAMES: raise ValueError( @@ -248,6 +246,13 @@ def __init__( self.semi_major_axis = semi_major_axis self.semi_minor_axis = semi_minor_axis + @property + @classmethod + @abstractmethod + def grid_mapping_name(cls): + """The value of the 'grid_mapping_name' attribute.""" + return + def __repr__(self): """x.__repr__() <==> repr(x)""" # Report parent GridMapping class to indicate classification, @@ -271,12 +276,6 @@ def __hash__(self, other): """The rich comparison operator ``==``.""" return hash(self.get_proj_string()) - @property - @abstractmethod - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - pass - @property @abstractmethod def proj_id(self): @@ -531,10 +530,7 @@ class AlbersEqualArea(ConicGridMapping): """ - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "albers_conical_equal_area" + grid_mapping_name = "albers_conical_equal_area" @property def proj_id(self): @@ -592,10 +588,7 @@ class AzimuthalEquidistant(AzimuthalGridMapping): """ - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "azimuthal_equidistant" + grid_mapping_name = "azimuthal_equidistant" @property def proj_id(self): @@ -677,6 +670,8 @@ class Geostationary(PerspectiveGridMapping): """ + grid_mapping_name = "geostationary" + def __init__( self, perspective_point_height, @@ -712,11 +707,6 @@ def __init__( self.sweep_angle_axis = sweep_angle_axis.lower() self.fixed_angle_axis = fixed_angle_axis.lower() - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "geostationary" - @property def proj_id(self): """The PROJ projection identifier shorthand name.""" @@ -773,10 +763,7 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): """ - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "lambert_azimuthal_equal_area" + grid_mapping_name = "lambert_azimuthal_equal_area" @property def proj_id(self): @@ -848,10 +835,7 @@ class LambertConformalConic(ConicGridMapping): """ - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "lambert_conformal_conic" + grid_mapping_name = "lambert_conformal_conic" @property def proj_id(self): @@ -920,6 +904,8 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): """ + grid_mapping_name = "lambert_cylindrical_equal_area" + def __init__( self, false_easting=0.0, @@ -937,11 +923,6 @@ def __init__( scale_factor_at_projection_origin ) - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "lambert_cylindrical_equal_area" - @property def proj_id(self): """The PROJ projection identifier shorthand name.""" @@ -1009,6 +990,8 @@ class Mercator(CylindricalGridMapping): """ + grid_mapping_name = "mercator" + def __init__( self, false_easting=0.0, @@ -1026,11 +1009,6 @@ def __init__( scale_factor_at_projection_origin ) - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "mercator" - @property def proj_id(self): """The PROJ projection identifier shorthand name.""" @@ -1101,6 +1079,8 @@ class ObliqueMercator(CylindricalGridMapping): """ + grid_mapping_name = "oblique_mercator" + def __init__( self, azimuth_of_central_line=0.0, @@ -1120,11 +1100,6 @@ def __init__( scale_factor_at_projection_origin ) - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "oblique_mercator" - @property def proj_id(self): """The PROJ projection identifier shorthand name.""" @@ -1181,10 +1156,7 @@ class Orthographic(AzimuthalGridMapping): """ - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "orthographic" + grid_mapping_name = "orthographic" @property def proj_id(self): @@ -1269,6 +1241,8 @@ class PolarStereographic(AzimuthalGridMapping): """ + grid_mapping_name = "polar_stereographic" + def __init__( self, latitude_of_projection_origin=0.0, @@ -1308,11 +1282,6 @@ def __init__( scale_factor_at_projection_origin ) - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "polar_stereographic" - @property def proj_id(self): """The PROJ projection identifier shorthand name.""" @@ -1365,6 +1334,8 @@ class RotatedLatitudeLongitude(LatLonGridMapping): """ + grid_mapping_name = "rotated_latitude_longitude" + def __init__( self, grid_north_pole_latitude, @@ -1378,11 +1349,6 @@ def __init__( self.grid_north_pole_longitude = grid_north_pole_longitude self.north_pole_grid_longitude = north_pole_grid_longitude - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "rotated_latitude_longitude" - @property def proj_id(self): """The PROJ projection identifier shorthand name.""" @@ -1409,10 +1375,7 @@ class LatitudeLongitude(LatLonGridMapping): """ - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "latitude_longitude" + grid_mapping_name = "latitude_longitude" @property def proj_id(self): @@ -1462,6 +1425,8 @@ class Sinusoidal(GridMapping): """ + grid_mapping_name = "sinusoidal" + def __init__( self, longitude_of_projection_origin=0.0, @@ -1475,11 +1440,6 @@ def __init__( self.false_easting = false_easting self.false_northing = false_northing - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "sinusoidal" - @property def proj_id(self): """The PROJ projection identifier shorthand name.""" @@ -1540,6 +1500,8 @@ class Stereographic(AzimuthalGridMapping): """ + grid_mapping_name = "stereographic" + def __init__( self, false_easting=0.0, @@ -1561,11 +1523,6 @@ def __init__( scale_factor_at_projection_origin ) - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "stereographic" - @property def proj_id(self): """The PROJ projection identifier shorthand name.""" @@ -1626,6 +1583,8 @@ class TransverseMercator(CylindricalGridMapping): """ + grid_mapping_name = "transverse_mercator" + def __init__( self, scale_factor_at_central_meridian=1.0, @@ -1643,11 +1602,6 @@ def __init__( self.longitude_of_central_meridian = longitude_of_central_meridian self.latitude_of_projection_origin = latitude_of_projection_origin - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "transverse_mercator" - @property def proj_id(self): """The PROJ projection identifier shorthand name.""" @@ -1709,10 +1663,7 @@ class VerticalPerspective(PerspectiveGridMapping): """ - @property - def grid_mapping_name(self): - """The value of the 'grid_mapping_name' attribute.""" - return "vertical_perspective" + grid_mapping_name = "vertical_perspective" @property def proj_id(self): @@ -1735,7 +1686,7 @@ def get_proj_string(self): LatLonGridMapping, PerspectiveGridMapping, ) -# Representing all Grid Mappings repsented by the CF Conventions (APpendix F) +# Representing all Grid Mappings repsented by the CF Conventions (Appendix F) _all_concrete_grid_mappings = ( AlbersEqualArea, AzimuthalEquidistant, @@ -1754,3 +1705,14 @@ def get_proj_string(self): TransverseMercator, VerticalPerspective, ) + + +def _get_cf_grid_mapping_from_name(gm_name): + """TODO.""" + cf_supported_gm_names = { + gm.grid_mapping_name: gm for gm in _all_concrete_grid_mappings + } + if gm_name in cf_supported_gm_names: + return cf_supported_gm_names[gm_name] + else: + return diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 587c88377e..68374c1046 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -60,7 +60,7 @@ class GridMappingsTest(unittest.TestCase): parameters={ "grid_mapping_name": "albers_conical_equal_area", "standard_parallel": [10, 10], - "longitude_of_projection_origin":45.0, + "longitude_of_projection_origin": 45.0, "false_easting": -1000, "false_northing": 500, } @@ -106,12 +106,18 @@ def test_grid_mapping__repr__str__(self): repr(g) str(g) - def test_grid_mapping_find_gm_class(self): + def test_grid_mapping__get_cf_grid_mapping_from_name(self): """TODO.""" - for f in self.f_with_gm: - gms = f.domain.get_grid_mappings() - print(gms) - # TODO test that matches with GM class + for gm_name, cf_gm_class in { + "vertical_perspective": cf.VerticalPerspective, + "oblique_mercator": cf.ObliqueMercator, + "albers_conical_equal_area": cf.AlbersEqualArea, + "lambert_conformal_conic": cf.LambertConformalConic, + "some_unsupported_name": None, + }.items(): + self.assertEqual( + cf._get_cf_grid_mapping_from_name(gm_name), cf_gm_class + ) if __name__ == "__main__": From 3f965274b008e4e06fef7d7b17b0042a769010d6 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 11 Aug 2023 18:15:27 +0100 Subject: [PATCH 33/97] Convert proj_id property to class attribute --- cf/gridmappings.py | 109 ++++++++++----------------------------------- 1 file changed, 23 insertions(+), 86 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 23f5676251..1c8d21cd3d 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -253,6 +253,13 @@ def grid_mapping_name(cls): """The value of the 'grid_mapping_name' attribute.""" return + @property + @classmethod + @abstractmethod + def proj_id(cls): + """The PROJ projection identifier shorthand name.""" + return + def __repr__(self): """x.__repr__() <==> repr(x)""" # Report parent GridMapping class to indicate classification, @@ -276,12 +283,6 @@ def __hash__(self, other): """The rich comparison operator ``==``.""" return hash(self.get_proj_string()) - @property - @abstractmethod - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - pass - @abstractmethod def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" @@ -531,11 +532,7 @@ class AlbersEqualArea(ConicGridMapping): """ grid_mapping_name = "albers_conical_equal_area" - - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "aea" + proj_id = "aea" def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" @@ -589,11 +586,7 @@ class AzimuthalEquidistant(AzimuthalGridMapping): """ grid_mapping_name = "azimuthal_equidistant" - - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "aeqd" + proj_id = "aeqd" def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" @@ -671,6 +664,7 @@ class Geostationary(PerspectiveGridMapping): """ grid_mapping_name = "geostationary" + proj_id = "geos" def __init__( self, @@ -707,11 +701,6 @@ def __init__( self.sweep_angle_axis = sweep_angle_axis.lower() self.fixed_angle_axis = fixed_angle_axis.lower() - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "geos" - def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS @@ -764,11 +753,7 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): """ grid_mapping_name = "lambert_azimuthal_equal_area" - - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "laea" + proj_id = "laea" def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" @@ -836,11 +821,7 @@ class LambertConformalConic(ConicGridMapping): """ grid_mapping_name = "lambert_conformal_conic" - - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "lcc" + proj_id = "lcc" def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" @@ -905,6 +886,7 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): """ grid_mapping_name = "lambert_cylindrical_equal_area" + proj_id = "cea" def __init__( self, @@ -923,11 +905,6 @@ def __init__( scale_factor_at_projection_origin ) - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "cea" - def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS @@ -991,6 +968,7 @@ class Mercator(CylindricalGridMapping): """ grid_mapping_name = "mercator" + proj_id = "merc" def __init__( self, @@ -1009,11 +987,6 @@ def __init__( scale_factor_at_projection_origin ) - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "merc" - def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS @@ -1080,6 +1053,7 @@ class ObliqueMercator(CylindricalGridMapping): """ grid_mapping_name = "oblique_mercator" + proj_id = "omerc" def __init__( self, @@ -1100,11 +1074,6 @@ def __init__( scale_factor_at_projection_origin ) - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "omerc" - def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS @@ -1157,11 +1126,7 @@ class Orthographic(AzimuthalGridMapping): """ grid_mapping_name = "orthographic" - - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "ortho" + proj_id = "ortho" def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" @@ -1242,6 +1207,7 @@ class PolarStereographic(AzimuthalGridMapping): """ grid_mapping_name = "polar_stereographic" + proj_id = "ups" def __init__( self, @@ -1282,11 +1248,6 @@ def __init__( scale_factor_at_projection_origin ) - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "ups" - def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS @@ -1335,6 +1296,7 @@ class RotatedLatitudeLongitude(LatLonGridMapping): """ grid_mapping_name = "rotated_latitude_longitude" + proj_id = "latlong" def __init__( self, @@ -1349,11 +1311,6 @@ def __init__( self.grid_north_pole_longitude = grid_north_pole_longitude self.north_pole_grid_longitude = north_pole_grid_longitude - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "latlong" - def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS @@ -1376,11 +1333,7 @@ class LatitudeLongitude(LatLonGridMapping): """ grid_mapping_name = "latitude_longitude" - - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "latlong" + proj_id = "latlong" def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" @@ -1426,6 +1379,7 @@ class Sinusoidal(GridMapping): """ grid_mapping_name = "sinusoidal" + proj_id = "sinu" def __init__( self, @@ -1440,11 +1394,6 @@ def __init__( self.false_easting = false_easting self.false_northing = false_northing - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "sinu" - def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS @@ -1501,6 +1450,7 @@ class Stereographic(AzimuthalGridMapping): """ grid_mapping_name = "stereographic" + proj_id = "stere" def __init__( self, @@ -1523,11 +1473,6 @@ def __init__( scale_factor_at_projection_origin ) - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "stere" - def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS @@ -1584,6 +1529,7 @@ class TransverseMercator(CylindricalGridMapping): """ grid_mapping_name = "transverse_mercator" + proj_id = "tmerc" def __init__( self, @@ -1602,11 +1548,6 @@ def __init__( self.longitude_of_central_meridian = longitude_of_central_meridian self.latitude_of_projection_origin = latitude_of_projection_origin - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "tmerc" - def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS @@ -1664,11 +1605,7 @@ class VerticalPerspective(PerspectiveGridMapping): """ grid_mapping_name = "vertical_perspective" - - @property - def proj_id(self): - """The PROJ projection identifier shorthand name.""" - return "nsper" + proj_id = "nsper" def get_proj_string(self): """The value of the PROJ proj-string defining the projection.""" From 23efc2748938deef3567fb759cbb71cf3cfc9eda Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 14 Aug 2023 12:08:12 +0100 Subject: [PATCH 34/97] Finish units conversion in gridmappings module --- cf/gridmappings.py | 79 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 1c8d21cd3d..b0fc842d7d 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -1,3 +1,4 @@ +import re from abc import ABC, abstractmethod from pyproj import CRS @@ -101,13 +102,81 @@ def _convert_units_cf_to_proj(cf_units): - """Take CF units and convert them to equivalent units under PROJ.""" - pass + """Take CF units and convert them to equivalent units under PROJ. + Note that PROJ units for latitude and longitude are in + units of decimal degrees, where forming a string by adding + a suffix character indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is usually 0.0 decimal degrees. For more information, see: -def _convert_units_proj_to_cf(cf_units): - """Take units used in PROJ and convert them to CF units.""" - pass + https://proj.org/en/9.2/usage/projections.html#projection-units + + TODO finish docs + + """ + value = None + proj_units = None + return value, proj_units + + +def _convert_units_proj_to_cf(proj_val_with_units, context): + """Take units used in PROJ and convert them to CF units. + + Note that PROJ units for latitude and longitude are in + units of decimal degrees, where forming a string by adding + a suffix character indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. The default + is usually 0.0 decimal degrees. For more information, see: + + https://proj.org/en/9.2/usage/projections.html#projection-units + + TODO finish docs + + """ + cf_compatible = True # unless find otherwise (True unless proven False) + if context == "lat": + cf_units = "degrees_north" + elif context == "lon": + cf_units = "degrees_east" + else: + # From the CF Conventions Document (3.1. Units): + # "The unit degrees is also allowed on coordinate variables such as " + # "the latitude and longitude coordinates of a transformed grid." + cf_units = "degrees" + + # Only valid input is a valid float or integer (digit with zero or one + # decimal point only) optionally followed by a single suffix letter + # indicating decimal degrees or radians with PROJ. Be strict about an + # exact regex match, because anything not following the pattern (e.g. + # something with extra letters) is ambiguous w.r.t. units. + valid_form = re.compile("(\d+(\.\d+)?)([rRdD°]?)") + form = re.fullmatch(valid_form, proj_val_with_units) + if form: + if len(form.groups()) == 3: + value, _, suffix = form.groups()[2] + else: + value = form.groups() + suffix = None + if suffix in ("r", "R"): # radians units + cf_units = "radians" + # Convert to decimal so we can store the degree_X form from context: + elif suffix and suffix not in ("d", "D", "°"): # 'decimal degrees' units + pass + else: + cf_compatible = False + else: + cf_compatible = False + + if not cf_units_found: + raise ValueError( + f"Input PROJ input not valid: {proj_val_with_units}. " + "Ensure valid PROJ units are supplied." + ) + + return value, cf.Units(cf_units) def _make_proj_string_comp(spec): From 3acae29b457a12542d5d57aebf02f41bda2129ea Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Tue, 15 Aug 2023 14:25:17 +0100 Subject: [PATCH 35/97] Complete GM-related unit conversion minus type equivalency --- cf/__init__.py | 1 + cf/gridmappings.py | 26 +++++++++++++++--------- cf/test/test_gridmappings.py | 38 +++++++++++++++++++++++++++++++++--- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/cf/__init__.py b/cf/__init__.py index 8d27d48a21..469aa0e136 100644 --- a/cf/__init__.py +++ b/cf/__init__.py @@ -349,6 +349,7 @@ _all_abstract_grid_mappings, _all_concrete_grid_mappings, _get_cf_grid_mapping_from_name, + _convert_units_proj_to_cf, ) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index b0fc842d7d..83dfd9e443 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -1,6 +1,9 @@ import re from abc import ABC, abstractmethod +from .data import Data +from .units import Units + from pyproj import CRS PROJ_PREFIX = "+proj" @@ -121,7 +124,7 @@ def _convert_units_cf_to_proj(cf_units): return value, proj_units -def _convert_units_proj_to_cf(proj_val_with_units, context): +def _convert_units_proj_to_cf(proj_val_with_units, context=None): """Take units used in PROJ and convert them to CF units. Note that PROJ units for latitude and longitude are in @@ -143,20 +146,27 @@ def _convert_units_proj_to_cf(proj_val_with_units, context): cf_units = "degrees_east" else: # From the CF Conventions Document (3.1. Units): - # "The unit degrees is also allowed on coordinate variables such as " - # "the latitude and longitude coordinates of a transformed grid." + # "The COARDS convention prohibits the unit degrees altogether, + # but this unit is not forbidden by the CF convention because + # it may in fact be appropriate for a variable containing, say, + # solar zenith angle. The unit degrees is also allowed on + # coordinate variables such as the latitude and longitude + # coordinates of a transformed grid. In this case the coordinate + # values are not true latitudes and longitudes which must always + # be identified using the more specific forms of degrees as + # described in Section 4.1. cf_units = "degrees" # Only valid input is a valid float or integer (digit with zero or one # decimal point only) optionally followed by a single suffix letter # indicating decimal degrees or radians with PROJ. Be strict about an # exact regex match, because anything not following the pattern (e.g. - # something with extra letters) is ambiguous w.r.t. units. + # something with extra letters) will be ambiguous for PROJ units. valid_form = re.compile("(\d+(\.\d+)?)([rRdD°]?)") form = re.fullmatch(valid_form, proj_val_with_units) if form: if len(form.groups()) == 3: - value, _, suffix = form.groups()[2] + value, _, suffix = form.groups() else: value = form.groups() suffix = None @@ -164,19 +174,17 @@ def _convert_units_proj_to_cf(proj_val_with_units, context): cf_units = "radians" # Convert to decimal so we can store the degree_X form from context: elif suffix and suffix not in ("d", "D", "°"): # 'decimal degrees' units - pass - else: cf_compatible = False else: cf_compatible = False - if not cf_units_found: + if not cf_compatible: raise ValueError( f"Input PROJ input not valid: {proj_val_with_units}. " "Ensure valid PROJ units are supplied." ) - return value, cf.Units(cf_units) + return Data(value, Units(cf_units)) def _make_proj_string_comp(spec): diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 68374c1046..b7c1e34ca7 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -2,12 +2,12 @@ import faulthandler import unittest -# import numpy as np - -faulthandler.enable() # to debug seg faults and timeouts +import numpy as np import cf +faulthandler.enable() # to debug seg faults and timeouts + pyproj_imported = False try: import pyproj @@ -119,6 +119,38 @@ def test_grid_mapping__get_cf_grid_mapping_from_name(self): cf._get_cf_grid_mapping_from_name(gm_name), cf_gm_class ) + def test_grid_mapping__convert_units_proj_to_cf(self): + """TODO.""" + for input_with_correct_output in [ + # Check float value and no suffix + (("45.0", None), cf.Data(45.0, units="degrees")), + (("45.0", "lat"), cf.Data(45.0, units="degrees_north")), + (("45.0", "lon"), cf.Data(45.0, units="degrees_east")), + # Check integer value and "d" suffix + (("10d", None), cf.Data(10, units="degrees")), + (("10d", "lat"), cf.Data(10, units="degrees_north")), + (("10d", "lon"), cf.Data(10, units="degrees_east")), + # Check >180 float and "D" suffix + (("200.123D", None), cf.Data(200.123, units="degrees")), + (("200.123D", "lat"), cf.Data(200.123, units="degrees_north")), + (("200.123D", "lon"), cf.Data(200.123, units="degrees_east")), + # Check "R" suffix + ((f"{0.5 * np.pi}R", None), cf.Data(90, units="degrees")), + ((f"{0.5 * np.pi}R", "lat"), cf.Data(90, units="degrees_north")), + ((f"{0.5 * np.pi}R", "lon"), cf.Data(90, units="degrees_east")), + # Check >360 degrees (full revolution) and "r" suffix + ((f"{3.0 * np.pi}r", None), cf.Data(180, units="degrees")), + ((f"{3.0 * np.pi}r", "lat"), cf.Data(180, units="degrees_north")), + ((f"{3.0 * np.pi}r", "lon"), cf.Data(180, units="degrees_east")), + ]: + _input, correct_output = input_with_correct_output + proj_arg, context_arg = _input + #print("ARGS OF:", _input, correct_output, proj_arg, context_arg) + d = cf._convert_units_proj_to_cf(proj_arg, context_arg) + #print("EXPECT:", correct_output) + #print("GET:", d) + #print() + self.assertTrue(d.equals(correct_output, verbose=2)) if __name__ == "__main__": print("Run date:", datetime.datetime.now()) From 57ee19d51ff39617289466dd1f4dd7204a07ef9e Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Tue, 15 Aug 2023 15:38:07 +0100 Subject: [PATCH 36/97] Apply type processing to units conversion to fix most new tests --- cf/gridmappings.py | 18 +++++++++++++++--- cf/test/test_gridmappings.py | 8 ++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 83dfd9e443..042e5d4d11 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -165,11 +165,17 @@ def _convert_units_proj_to_cf(proj_val_with_units, context=None): valid_form = re.compile("(\d+(\.\d+)?)([rRdD°]?)") form = re.fullmatch(valid_form, proj_val_with_units) if form: + suffix, float_comp = None, None if len(form.groups()) == 3: - value, _, suffix = form.groups() + value, float_comp, suffix = form.groups() + #if float_comp: + # is_flaot= True + #print("FOR", form, "GET", value, is_float, suffix) + elif len(form.groups()) == 2: + value, float_comp = form.groups() else: value = form.groups() - suffix = None + if suffix in ("r", "R"): # radians units cf_units = "radians" # Convert to decimal so we can store the degree_X form from context: @@ -184,7 +190,13 @@ def _convert_units_proj_to_cf(proj_val_with_units, context=None): "Ensure valid PROJ units are supplied." ) - return Data(value, Units(cf_units)) + # Convert string value to relevant numeric form + if float_comp: + numeric_value = float(value) + else: + numeric_value = int(value) + + return Data(numeric_value, Units(cf_units)) def _make_proj_string_comp(spec): diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index b7c1e34ca7..bd44294c67 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -145,11 +145,11 @@ def test_grid_mapping__convert_units_proj_to_cf(self): ]: _input, correct_output = input_with_correct_output proj_arg, context_arg = _input - #print("ARGS OF:", _input, correct_output, proj_arg, context_arg) + print("ARGS OF:", _input, correct_output, proj_arg, context_arg) d = cf._convert_units_proj_to_cf(proj_arg, context_arg) - #print("EXPECT:", correct_output) - #print("GET:", d) - #print() + print("EXPECT:", correct_output) + print("GET:", d) + print() self.assertTrue(d.equals(correct_output, verbose=2)) if __name__ == "__main__": From b37bb60c4ed2156c7d18a20937172ad51655a5cc Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Wed, 16 Aug 2023 13:17:52 +0100 Subject: [PATCH 37/97] Update logic in previous function for better unpacking --- cf/gridmappings.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 042e5d4d11..6b25c0c0ee 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -165,20 +165,19 @@ def _convert_units_proj_to_cf(proj_val_with_units, context=None): valid_form = re.compile("(\d+(\.\d+)?)([rRdD°]?)") form = re.fullmatch(valid_form, proj_val_with_units) if form: - suffix, float_comp = None, None - if len(form.groups()) == 3: - value, float_comp, suffix = form.groups() - #if float_comp: - # is_flaot= True - #print("FOR", form, "GET", value, is_float, suffix) - elif len(form.groups()) == 2: - value, float_comp = form.groups() + comps = form.groups() + suffix = None + if len(comps) == 3: + value, float_comp, suffix = comps else: - value = form.groups() + value, *float_comp = comps + + print("%%%%%%%", value, float_comp, suffix) if suffix in ("r", "R"): # radians units cf_units = "radians" # Convert to decimal so we can store the degree_X form from context: + # TODO elif suffix and suffix not in ("d", "D", "°"): # 'decimal degrees' units cf_compatible = False else: From 02aaa1a8781509f9d0929c9ed602a636e4d9bfca Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Wed, 16 Aug 2023 14:52:57 +0100 Subject: [PATCH 38/97] Fix unit conversion processing of radians --- cf/gridmappings.py | 22 ++++++++++++---------- cf/test/test_gridmappings.py | 12 ++++++------ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 6b25c0c0ee..adcbfbece6 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -172,12 +172,20 @@ def _convert_units_proj_to_cf(proj_val_with_units, context=None): else: value, *float_comp = comps - print("%%%%%%%", value, float_comp, suffix) + # Convert string value to relevant numeric form + if float_comp: + numeric_value = float(value) + else: + numeric_value = int(value) if suffix in ("r", "R"): # radians units - cf_units = "radians" - # Convert to decimal so we can store the degree_X form from context: - # TODO + if context: + # Convert so we can store the degree_X form of the lat/lon context: + numeric_value = Units.conform( + numeric_value, Units("radians"), Units("degrees")) + else: # if no lat/lon context, leave as radians to avoid rounding etc. + cf_units = "radians" + elif suffix and suffix not in ("d", "D", "°"): # 'decimal degrees' units cf_compatible = False else: @@ -189,12 +197,6 @@ def _convert_units_proj_to_cf(proj_val_with_units, context=None): "Ensure valid PROJ units are supplied." ) - # Convert string value to relevant numeric form - if float_comp: - numeric_value = float(value) - else: - numeric_value = int(value) - return Data(numeric_value, Units(cf_units)) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index bd44294c67..6d87db11a0 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -135,13 +135,13 @@ def test_grid_mapping__convert_units_proj_to_cf(self): (("200.123D", "lat"), cf.Data(200.123, units="degrees_north")), (("200.123D", "lon"), cf.Data(200.123, units="degrees_east")), # Check "R" suffix - ((f"{0.5 * np.pi}R", None), cf.Data(90, units="degrees")), - ((f"{0.5 * np.pi}R", "lat"), cf.Data(90, units="degrees_north")), - ((f"{0.5 * np.pi}R", "lon"), cf.Data(90, units="degrees_east")), + ((f"{0.5 * np.pi}R", None), cf.Data(0.5 * np.pi, units="radians")), + ((f"{0.5 * np.pi}R", "lat"), cf.Data(90.0, units="degrees_north")), + ((f"{0.5 * np.pi}R", "lon"), cf.Data(90.0, units="degrees_east")), # Check >360 degrees (full revolution) and "r" suffix - ((f"{3.0 * np.pi}r", None), cf.Data(180, units="degrees")), - ((f"{3.0 * np.pi}r", "lat"), cf.Data(180, units="degrees_north")), - ((f"{3.0 * np.pi}r", "lon"), cf.Data(180, units="degrees_east")), + ((f"{3.0 * np.pi}r", None), cf.Data(3.0 * np.pi, units="radians")), + ((f"{3.0 * np.pi}r", "lat"), cf.Data(540.0, units="degrees_north")), + ((f"{3.0 * np.pi}r", "lon"), cf.Data(540.0, units="degrees_east")), ]: _input, correct_output = input_with_correct_output proj_arg, context_arg = _input From 6e60723a4a0076a063f2b0768664ddc30adbd31b Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Thu, 17 Aug 2023 20:53:37 +0100 Subject: [PATCH 39/97] Include negative values as value PROJ angular values --- cf/gridmappings.py | 2 +- cf/test/test_gridmappings.py | 31 +++++++++++++++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index adcbfbece6..06717cb0cf 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -162,7 +162,7 @@ def _convert_units_proj_to_cf(proj_val_with_units, context=None): # indicating decimal degrees or radians with PROJ. Be strict about an # exact regex match, because anything not following the pattern (e.g. # something with extra letters) will be ambiguous for PROJ units. - valid_form = re.compile("(\d+(\.\d+)?)([rRdD°]?)") + valid_form = re.compile("(-?\d+(\.\d+)?)([rRdD°]?)") form = re.fullmatch(valid_form, proj_val_with_units) if form: comps = form.groups() diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 6d87db11a0..588196f201 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -126,6 +126,18 @@ def test_grid_mapping__convert_units_proj_to_cf(self): (("45.0", None), cf.Data(45.0, units="degrees")), (("45.0", "lat"), cf.Data(45.0, units="degrees_north")), (("45.0", "lon"), cf.Data(45.0, units="degrees_east")), + # Check integer and no suffix + (("100", None), cf.Data(100, units="degrees")), + (("100", "lat"), cf.Data(100, units="degrees_north")), + (("100", "lon"), cf.Data(100, units="degrees_east")), + # Check "R" suffix + ((f"{0.5 * np.pi}R", None), cf.Data(0.5 * np.pi, units="radians")), + ((f"{0.5 * np.pi}R", "lat"), cf.Data(90.0, units="degrees_north")), + ((f"{0.5 * np.pi}R", "lon"), cf.Data(90.0, units="degrees_east")), + # Check >360 degrees (over a full revolution) and "r" suffix + ((f"{3.0 * np.pi}r", None), cf.Data(3.0 * np.pi, units="radians")), + ((f"{3.0 * np.pi}r", "lat"), cf.Data(540.0, units="degrees_north")), + ((f"{3.0 * np.pi}r", "lon"), cf.Data(540.0, units="degrees_east")), # Check integer value and "d" suffix (("10d", None), cf.Data(10, units="degrees")), (("10d", "lat"), cf.Data(10, units="degrees_north")), @@ -134,22 +146,13 @@ def test_grid_mapping__convert_units_proj_to_cf(self): (("200.123D", None), cf.Data(200.123, units="degrees")), (("200.123D", "lat"), cf.Data(200.123, units="degrees_north")), (("200.123D", "lon"), cf.Data(200.123, units="degrees_east")), - # Check "R" suffix - ((f"{0.5 * np.pi}R", None), cf.Data(0.5 * np.pi, units="radians")), - ((f"{0.5 * np.pi}R", "lat"), cf.Data(90.0, units="degrees_north")), - ((f"{0.5 * np.pi}R", "lon"), cf.Data(90.0, units="degrees_east")), - # Check >360 degrees (full revolution) and "r" suffix - ((f"{3.0 * np.pi}r", None), cf.Data(3.0 * np.pi, units="radians")), - ((f"{3.0 * np.pi}r", "lat"), cf.Data(540.0, units="degrees_north")), - ((f"{3.0 * np.pi}r", "lon"), cf.Data(540.0, units="degrees_east")), + # Check negative numeric value and "°" suffix + (("-70.5°", None), cf.Data(-70.5, units="degrees")), + (("-70.5°", "lat"), cf.Data(-70.5, units="degrees_north")), + (("-70.5°", "lon"), cf.Data(-70.5, units="degrees_east")), ]: _input, correct_output = input_with_correct_output - proj_arg, context_arg = _input - print("ARGS OF:", _input, correct_output, proj_arg, context_arg) - d = cf._convert_units_proj_to_cf(proj_arg, context_arg) - print("EXPECT:", correct_output) - print("GET:", d) - print() + d = cf._convert_units_proj_to_cf(*_input) self.assertTrue(d.equals(correct_output, verbose=2)) if __name__ == "__main__": From b1e817dc79a32d4527c0f0098ba289f38d57e0a4 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Thu, 17 Aug 2023 22:25:55 +0100 Subject: [PATCH 40/97] Tidy PROJ->CF unit conversion --- cf/__init__.py | 2 +- cf/gridmappings.py | 43 +++++++++++++++++------------------- cf/test/test_gridmappings.py | 2 +- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/cf/__init__.py b/cf/__init__.py index 469aa0e136..641df21e42 100644 --- a/cf/__init__.py +++ b/cf/__init__.py @@ -346,10 +346,10 @@ Stereographic, TransverseMercator, VerticalPerspective, + convert_proj_angular_data_to_cf, _all_abstract_grid_mappings, _all_concrete_grid_mappings, _get_cf_grid_mapping_from_name, - _convert_units_proj_to_cf, ) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 06717cb0cf..69972cc5ce 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -104,39 +104,36 @@ DUMMY_PARAMS = {"a": "b", "c": 0.0} # TODOPARAMETERS, drop this -def _convert_units_cf_to_proj(cf_units): - """Take CF units and convert them to equivalent units under PROJ. +def convert_proj_angular_data_to_cf(proj_data, context=None): + """Take a PROJ angular data component and convert it to CF Data with CF Units. Note that PROJ units for latitude and longitude are in units of decimal degrees, where forming a string by adding a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is usually 0.0 decimal degrees. For more information, see: + of 'd', 'D' or '°' confirm units of decimal degrees. - https://proj.org/en/9.2/usage/projections.html#projection-units - - TODO finish docs - - """ - value = None - proj_units = None - return value, proj_units + .. versionadded:: GMVER + :Parameters: -def _convert_units_proj_to_cf(proj_val_with_units, context=None): - """Take units used in PROJ and convert them to CF units. + proj_data: `str` + The PROJ angular data component, for example "90", "90.0", + "90.0D", "1.0R", or "1r". - Note that PROJ units for latitude and longitude are in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is usually 0.0 decimal degrees. For more information, see: + For details on valid PROJ data components in PROJ strings, + notably indicating units, see: - https://proj.org/en/9.2/usage/projections.html#projection-units + https://proj.org/en/9.2/usage/projections.html#projection-units - TODO finish docs + context: `str` or `None`, optional + The physical context of the conversion, where 'lat' indicates + a latitude value and 'lon' indicates a longitude, such that + indication of either context will return cf.Data with values + having units appropriate to that context, namely 'degrees_north' + or 'degrees_east' respectively. If None, 'degrees' or 'radians' + (depending on the input PROJ units) will be the units. + The default is None. """ cf_compatible = True # unless find otherwise (True unless proven False) @@ -163,7 +160,7 @@ def _convert_units_proj_to_cf(proj_val_with_units, context=None): # exact regex match, because anything not following the pattern (e.g. # something with extra letters) will be ambiguous for PROJ units. valid_form = re.compile("(-?\d+(\.\d+)?)([rRdD°]?)") - form = re.fullmatch(valid_form, proj_val_with_units) + form = re.fullmatch(valid_form, proj_data) if form: comps = form.groups() suffix = None diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 588196f201..afb04df252 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -152,7 +152,7 @@ def test_grid_mapping__convert_units_proj_to_cf(self): (("-70.5°", "lon"), cf.Data(-70.5, units="degrees_east")), ]: _input, correct_output = input_with_correct_output - d = cf._convert_units_proj_to_cf(*_input) + d = cf.convert_proj_angular_data_to_cf(*_input) self.assertTrue(d.equals(correct_output, verbose=2)) if __name__ == "__main__": From 49f67c7726dafd4c2238b25deaafca3554313ffe Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 18 Aug 2023 15:26:34 +0100 Subject: [PATCH 41/97] Add new data conversion CF->PROJ func w/ basic testing --- cf/__init__.py | 1 + cf/gridmappings.py | 102 +++++++++++++++++++++++++++++++---- cf/test/test_gridmappings.py | 87 +++++++++++++++++++----------- 3 files changed, 148 insertions(+), 42 deletions(-) diff --git a/cf/__init__.py b/cf/__init__.py index 641df21e42..79d160bdec 100644 --- a/cf/__init__.py +++ b/cf/__init__.py @@ -347,6 +347,7 @@ TransverseMercator, VerticalPerspective, convert_proj_angular_data_to_cf, + convert_cf_angular_data_to_proj, _all_abstract_grid_mappings, _all_concrete_grid_mappings, _get_cf_grid_mapping_from_name, diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 69972cc5ce..4333435282 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -1,11 +1,12 @@ +import itertools import re from abc import ABC, abstractmethod +from pyproj import CRS + from .data import Data from .units import Units -from pyproj import CRS - PROJ_PREFIX = "+proj" ALL_GRID_MAPPING_ATTR_NAMES = { "grid_mapping_name", @@ -105,7 +106,7 @@ def convert_proj_angular_data_to_cf(proj_data, context=None): - """Take a PROJ angular data component and convert it to CF Data with CF Units. + """Convert a PROJ angular data component into CF Data with CF Units. Note that PROJ units for latitude and longitude are in units of decimal degrees, where forming a string by adding @@ -135,6 +136,12 @@ def convert_proj_angular_data_to_cf(proj_data, context=None): (depending on the input PROJ units) will be the units. The default is None. + :Returns: + + `Data` + A cf.Data object with CF-compliant units that corresponds + to the PROJ data and the context provided. + """ cf_compatible = True # unless find otherwise (True unless proven False) if context == "lat": @@ -159,7 +166,7 @@ def convert_proj_angular_data_to_cf(proj_data, context=None): # indicating decimal degrees or radians with PROJ. Be strict about an # exact regex match, because anything not following the pattern (e.g. # something with extra letters) will be ambiguous for PROJ units. - valid_form = re.compile("(-?\d+(\.\d+)?)([rRdD°]?)") + valid_form = re.compile("(-?\d+(\.\d*)?)([rRdD°]?)") form = re.fullmatch(valid_form, proj_data) if form: comps = form.groups() @@ -177,26 +184,99 @@ def convert_proj_angular_data_to_cf(proj_data, context=None): if suffix in ("r", "R"): # radians units if context: - # Convert so we can store the degree_X form of the lat/lon context: + # Convert so we can apply degree_X form of the lat/lon context numeric_value = Units.conform( - numeric_value, Units("radians"), Units("degrees")) - else: # if no lat/lon context, leave as radians to avoid rounding etc. + numeric_value, Units("radians"), Units("degrees") + ) + else: # Otherwise leave as radians to avoid rounding etc. cf_units = "radians" - - elif suffix and suffix not in ("d", "D", "°"): # 'decimal degrees' units + elif suffix and suffix not in ("d", "D", "°"): # 'decimal degrees' cf_compatible = False else: cf_compatible = False if not cf_compatible: raise ValueError( - f"Input PROJ input not valid: {proj_val_with_units}. " - "Ensure valid PROJ units are supplied." + f"PROJ data input not valid: {proj_data}. Ensure a valid " + "PROJ value and optionally units are supplied." ) return Data(numeric_value, Units(cf_units)) +def convert_cf_angular_data_to_proj(data): + """Convert singleton angular CF Data into a PROJ data component. + + PROJ units for latitude and longitude are generally in units of + decimal degrees. + + .. versionadded:: GMVER + + :Parameters: + + data: `Data` + A cf.Data object of size 1 containing an angular value + with CF-compliant angular units, for example + cf.Data(45, units="degrees_north"). + + :Returns: + + `str` + A PROJ angular data component that corresponds + to the Data provided. + + """ + if data.size != 1: + raise ValueError( + f"Input cf.Data must have size 1, got size: {data.size}" + ) + + units = data.Units + if not units: + raise ValueError( + "Must provide cf.Data with units for unambiguous conversion." + ) + units_str = units.units + + degrees_unit_prefix = ["degree", "degrees"] + # Valid possibilities from 4.1. Latitude Coordinate: + # http://cfconventions.org/cf-conventions/ + # cf-conventions.html#latitude-coordinate + valid_cf_lat_units = [ + s + e + for s, e in itertools.product( + degrees_unit_prefix, ("_north", "_N", "N") + ) + ] + # Valid possibilities from 4.2. Longitude Coordinate, see: + # http://cfconventions.org/cf-conventions/ + # cf-conventions.html#longitude-coordinate + valid_cf_lon_units = [ + s + e + for s, e in itertools.product( + degrees_unit_prefix, ("_east", "_E", "E") + ) + ] + valid_degrees_units = ( + degrees_unit_prefix + valid_cf_lat_units + valid_cf_lon_units + ) + + if units_str in valid_degrees_units: + # No need for suffix 'D' for decimal degrees, as that is the default + # recognised when no suffix is given + proj_data = f"{data.data.array.item()}" + elif units_str == "radians": + proj_data = f"{data.data.array.item()}R" + else: + raise ValueError( + "Unrecognised angular units set on the cf.Data. Valid options " + f"are: {', '.join(valid_degrees_units)} and radians but got: " + f"{units_str}" + ) + + return proj_data + + def _make_proj_string_comp(spec): """Form a PROJ proj-string end from the given PROJ parameters. diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index afb04df252..79e2582e35 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -119,42 +119,67 @@ def test_grid_mapping__get_cf_grid_mapping_from_name(self): cf._get_cf_grid_mapping_from_name(gm_name), cf_gm_class ) - def test_grid_mapping__convert_units_proj_to_cf(self): - """TODO.""" + def test_grid_mapping_convert_proj_angular_data_to_cf(self): + """Test the 'convert_proj_angular_data_to_cf' function.""" for input_with_correct_output in [ - # Check float value and no suffix - (("45.0", None), cf.Data(45.0, units="degrees")), - (("45.0", "lat"), cf.Data(45.0, units="degrees_north")), - (("45.0", "lon"), cf.Data(45.0, units="degrees_east")), - # Check integer and no suffix - (("100", None), cf.Data(100, units="degrees")), - (("100", "lat"), cf.Data(100, units="degrees_north")), - (("100", "lon"), cf.Data(100, units="degrees_east")), - # Check "R" suffix - ((f"{0.5 * np.pi}R", None), cf.Data(0.5 * np.pi, units="radians")), - ((f"{0.5 * np.pi}R", "lat"), cf.Data(90.0, units="degrees_north")), - ((f"{0.5 * np.pi}R", "lon"), cf.Data(90.0, units="degrees_east")), - # Check >360 degrees (over a full revolution) and "r" suffix - ((f"{3.0 * np.pi}r", None), cf.Data(3.0 * np.pi, units="radians")), - ((f"{3.0 * np.pi}r", "lat"), cf.Data(540.0, units="degrees_north")), - ((f"{3.0 * np.pi}r", "lon"), cf.Data(540.0, units="degrees_east")), - # Check integer value and "d" suffix - (("10d", None), cf.Data(10, units="degrees")), - (("10d", "lat"), cf.Data(10, units="degrees_north")), - (("10d", "lon"), cf.Data(10, units="degrees_east")), - # Check >180 float and "D" suffix - (("200.123D", None), cf.Data(200.123, units="degrees")), - (("200.123D", "lat"), cf.Data(200.123, units="degrees_north")), - (("200.123D", "lon"), cf.Data(200.123, units="degrees_east")), - # Check negative numeric value and "°" suffix - (("-70.5°", None), cf.Data(-70.5, units="degrees")), - (("-70.5°", "lat"), cf.Data(-70.5, units="degrees_north")), - (("-70.5°", "lon"), cf.Data(-70.5, units="degrees_east")), - ]: + # Check float value and no suffix + (("45.0", None), cf.Data(45.0, units="degrees")), + (("45.0", "lat"), cf.Data(45.0, units="degrees_north")), + (("45.0", "lon"), cf.Data(45.0, units="degrees_east")), + # Check integer and no suffix + (("100", None), cf.Data(100, units="degrees")), + (("100", "lat"), cf.Data(100, units="degrees_north")), + (("100", "lon"), cf.Data(100, units="degrees_east")), + # Check >360 degrees (over a full revolution) and "r" suffix + ((f"{3.0 * np.pi}r", None), cf.Data(3.0 * np.pi, units="radians")), + ( + (f"{3.0 * np.pi}r", "lat"), + cf.Data(540.0, units="degrees_north"), + ), + ((f"{3.0 * np.pi}r", "lon"), cf.Data(540.0, units="degrees_east")), + # Check "R" suffix + ((f"{0.5 * np.pi}R", None), cf.Data(0.5 * np.pi, units="radians")), + ((f"{0.5 * np.pi}R", "lat"), cf.Data(90.0, units="degrees_north")), + ((f"{0.5 * np.pi}R", "lon"), cf.Data(90.0, units="degrees_east")), + # Check integer value and "d" suffix + (("10d", None), cf.Data(10, units="degrees")), + (("10d", "lat"), cf.Data(10, units="degrees_north")), + (("10d", "lon"), cf.Data(10, units="degrees_east")), + # Check >180 float and "D" suffix + (("200.123D", None), cf.Data(200.123, units="degrees")), + (("200.123D", "lat"), cf.Data(200.123, units="degrees_north")), + (("200.123D", "lon"), cf.Data(200.123, units="degrees_east")), + # Check negative numeric value and "°" suffix + (("-70.5°", None), cf.Data(-70.5, units="degrees")), + (("-70.5°", "lat"), cf.Data(-70.5, units="degrees_north")), + (("-70.5°", "lon"), cf.Data(-70.5, units="degrees_east")), + # Check zero and lack of digits after point edge cases + (("0", None), cf.Data(0, units="degrees")), + (("0.0", "lat"), cf.Data(0.0, units="degrees_north")), + (("-0.", "lon"), cf.Data(0.0, units="degrees_east")), + ]: _input, correct_output = input_with_correct_output d = cf.convert_proj_angular_data_to_cf(*_input) self.assertTrue(d.equals(correct_output, verbose=2)) + def test_grid_mapping_convert_cf_angular_data_to_proj(self): + """Test the 'convert_cf_angular_data_to_proj' function.""" + for input_with_correct_output in [ + # Check float and basic lat/lon-context free degree unit + (cf.Data(45.0, units="degrees"), "45.0"), + # Check integer and various units possible for lat/lon + (cf.Data(45, units="degrees_north"), "45"), + (cf.Data(45, units="degrees_N"), "45"), + (cf.Data(45, units="degreeN"), "45"), + (cf.Data(45, units="degrees_east"), "45"), + (cf.Data(45, units="degrees_E"), "45"), + (cf.Data(45, units="degreeE"), "45"), + ]: + _input, correct_output = input_with_correct_output + d = cf.convert_cf_angular_data_to_proj(_input) + self.assertEqual(d, correct_output) + + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) cf.environment() From c4acef3a860551111248644de18aea32c937c1c3 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 18 Aug 2023 18:53:58 +0100 Subject: [PATCH 42/97] Fix flake8 with ignore --- cf/test/test_gridmappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 79e2582e35..9366a8b950 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -10,7 +10,7 @@ pyproj_imported = False try: - import pyproj + import pyproj # noqa: F401 pyproj_imported = True except ImportError: From 4aa05c386499de25a2c402d4868399df6904ce3d Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 22 Aug 2023 11:42:57 +0100 Subject: [PATCH 43/97] Add to testing of PROJ->CF Data conversions w/ inverse mapping check --- cf/test/test_gridmappings.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 9366a8b950..3c8a812488 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -176,8 +176,25 @@ def test_grid_mapping_convert_cf_angular_data_to_proj(self): (cf.Data(45, units="degreeE"), "45"), ]: _input, correct_output = input_with_correct_output - d = cf.convert_cf_angular_data_to_proj(_input) - self.assertEqual(d, correct_output) + p = cf.convert_cf_angular_data_to_proj(_input) + self.assertEqual(p, correct_output) + + # Note that 'convert_cf_angular_data_to_proj' and + # 'convert_proj_angular_data_to_cf' are not strict inverse + # functions, since the former will convert to the *simplest* + # way to specify the PROJ input, namely with no suffix for + # degrees(_X) units and the 'R' suffix for radians, whereas + # the input might have 'D' or 'r' etc. instead. + # + # However, we check that inputs that are expected to be + # undone to their original form by operation of both + # functions, namely those with this 'simplest' PROJ form, + # do indeed get re-generated with operation of both: + for p in ("10", "-10", "0", "1R", "0.2R", "0.1R"): + p2 = cf.convert_cf_angular_data_to_proj( + cf.convert_proj_angular_data_to_cf(p) + ) + self.assertEqual(p, p2) if __name__ == "__main__": From 5a027c7e8cad5a9996e014ebf42ddacf1a440a98 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 22 Aug 2023 15:14:48 +0100 Subject: [PATCH 44/97] Extend PROJ<->CF data test --- cf/test/test_gridmappings.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 3c8a812488..f690ee0818 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -121,6 +121,8 @@ def test_grid_mapping__get_cf_grid_mapping_from_name(self): def test_grid_mapping_convert_proj_angular_data_to_cf(self): """Test the 'convert_proj_angular_data_to_cf' function.""" + + # Check representative valid inputs for input_with_correct_output in [ # Check float value and no suffix (("45.0", None), cf.Data(45.0, units="degrees")), @@ -164,6 +166,7 @@ def test_grid_mapping_convert_proj_angular_data_to_cf(self): def test_grid_mapping_convert_cf_angular_data_to_proj(self): """Test the 'convert_cf_angular_data_to_proj' function.""" + # Check representative valid inputs for input_with_correct_output in [ # Check float and basic lat/lon-context free degree unit (cf.Data(45.0, units="degrees"), "45.0"), @@ -174,11 +177,31 @@ def test_grid_mapping_convert_cf_angular_data_to_proj(self): (cf.Data(45, units="degrees_east"), "45"), (cf.Data(45, units="degrees_E"), "45"), (cf.Data(45, units="degreeE"), "45"), + # Check negative + (cf.Data(-0.1, units="degrees"), "-0.1"), + (cf.Data(-10, units="degrees"), "-10"), + # Check zero case + (cf.Data(0, units="degrees_north"), "0"), + (cf.Data(0.0, units="degrees_north"), "0.0"), + # Check radians units cases and >180 + (cf.Data(190, units="radians"), "190R"), + (cf.Data(190.0, units="radians"), "190.0R"), + # Check TODO + # TODO ]: _input, correct_output = input_with_correct_output p = cf.convert_cf_angular_data_to_proj(_input) self.assertEqual(p, correct_output) + # Check representative invalid inputs error correctly + for bad_input in [ + cf.Data([1, 2, 3]), # not singular (size 1) + cf.Data(45), # no units + cf.Data(45, "m"), # non-angular units + ]: + with self.assertRaises(ValueError): + cf.convert_cf_angular_data_to_proj(bad_input) + # Note that 'convert_cf_angular_data_to_proj' and # 'convert_proj_angular_data_to_cf' are not strict inverse # functions, since the former will convert to the *simplest* From 84a9d2113ca121f2ba42631949efc3e7e9fb53b8 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 23 Aug 2023 17:15:34 +0100 Subject: [PATCH 45/97] Extend inverse relation test for PROJ<->CF data --- cf/test/test_gridmappings.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index f690ee0818..c9a9a1e1d0 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -219,6 +219,16 @@ def test_grid_mapping_convert_cf_angular_data_to_proj(self): ) self.assertEqual(p, p2) + # With a lat or lon 'context' + p3 = cf.convert_cf_angular_data_to_proj( + cf.convert_proj_angular_data_to_cf(p, context="lat") + ) + self.assertEqual(p, p3) + p4 = cf.convert_cf_angular_data_to_proj( + cf.convert_proj_angular_data_to_cf(p, context="lon") + ) + self.assertEqual(p, p4) + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) From cfdba780ee4cae6afb8b7c73c1c92f8f39edabb9 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Thu, 24 Aug 2023 13:59:07 +0100 Subject: [PATCH 46/97] Fix test ready for inverse PROJ<->CF data test cases --- cf/test/test_gridmappings.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index c9a9a1e1d0..60f20fab92 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -213,21 +213,26 @@ def test_grid_mapping_convert_cf_angular_data_to_proj(self): # undone to their original form by operation of both # functions, namely those with this 'simplest' PROJ form, # do indeed get re-generated with operation of both: - for p in ("10", "-10", "0", "1R", "0.2R", "0.1R"): + for p in ("10", "-10", "10.11", "0", "1R", "0.2R", "-0.1R", "0R"): p2 = cf.convert_cf_angular_data_to_proj( cf.convert_proj_angular_data_to_cf(p) ) self.assertEqual(p, p2) - # With a lat or lon 'context' - p3 = cf.convert_cf_angular_data_to_proj( - cf.convert_proj_angular_data_to_cf(p, context="lat") - ) - self.assertEqual(p, p3) - p4 = cf.convert_cf_angular_data_to_proj( - cf.convert_proj_angular_data_to_cf(p, context="lon") - ) - self.assertEqual(p, p4) + # With a lat or lon 'context'. Only non-radians inputs will + # be re-generated since degrees_X gets converted back to the + # default degrees, so skip those in these test cases. + if not p.endswith("R"): + a = cf.convert_proj_angular_data_to_cf(p, context="lat") + p3 = cf.convert_cf_angular_data_to_proj( + cf.convert_proj_angular_data_to_cf(p, context="lat") + ) + self.assertEqual(p, p3) + + p4 = cf.convert_cf_angular_data_to_proj( + cf.convert_proj_angular_data_to_cf(p, context="lon") + ) + self.assertEqual(p, p4) if __name__ == "__main__": From 873ca07cb57b1832ae78609774a5fa085e3310f9 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Thu, 24 Aug 2023 18:47:44 +0100 Subject: [PATCH 47/97] Tidy convert_cf_angular_data_to_proj --- cf/gridmappings.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 4333435282..68731b8d47 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -239,27 +239,16 @@ def convert_cf_angular_data_to_proj(data): units_str = units.units degrees_unit_prefix = ["degree", "degrees"] - # Valid possibilities from 4.1. Latitude Coordinate: - # http://cfconventions.org/cf-conventions/ - # cf-conventions.html#latitude-coordinate - valid_cf_lat_units = [ + # Taken from 4.1. Latitude Coordinate and 4.2. Longitude Coordinate: + # http://cfconventions.org/cf-conventions/cf-conventions.html... + # ...#latitude-coordinate and ...#longitude-coordinate + valid_cf_lat_lon_units = [ s + e for s, e in itertools.product( - degrees_unit_prefix, ("_north", "_N", "N") + degrees_unit_prefix, ("_north", "_N", "N", "_east", "_E", "E") ) ] - # Valid possibilities from 4.2. Longitude Coordinate, see: - # http://cfconventions.org/cf-conventions/ - # cf-conventions.html#longitude-coordinate - valid_cf_lon_units = [ - s + e - for s, e in itertools.product( - degrees_unit_prefix, ("_east", "_E", "E") - ) - ] - valid_degrees_units = ( - degrees_unit_prefix + valid_cf_lat_units + valid_cf_lon_units - ) + valid_degrees_units = degrees_unit_prefix + valid_cf_lat_lon_units if units_str in valid_degrees_units: # No need for suffix 'D' for decimal degrees, as that is the default From 5378645584a3a3d98a600f0afe870dbee8a90053 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Fri, 25 Aug 2023 10:48:15 +0100 Subject: [PATCH 48/97] Extend get_grid_mappings class to return classes as option --- cf/domain.py | 28 +++++++++++++++------------- cf/field.py | 17 ++++++++++++++--- cf/test/test_Domain.py | 5 +++++ cf/test/test_Field.py | 7 +++++-- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/cf/domain.py b/cf/domain.py index 58373cf920..b36d44818c 100644 --- a/cf/domain.py +++ b/cf/domain.py @@ -17,6 +17,7 @@ indices_shape, parse_indices, ) +from .gridmappings import _get_cf_grid_mapping_from_name _empty_set = set() @@ -476,22 +477,20 @@ def get_data(self, default=ValueError(), _units=None, _fill_value=True): default, message=f"{self.__class__.__name__} has no data" ) - -def _get_cf_grid_mapping_from_name(gm_name): - """TODOSADIE.""" - cf_supported_gm_names = { - gm.grid_mapping_name: gm for gm in _all_concrete_grid_mappings - } - if gm_name in cf_supported_gm_names: - return cf_supported_gm_names[gm_name] - else: - return - - def get_grid_mappings(self): + def get_grid_mappings(self, as_class=False): """Returns coordinate conversions with grid mapping parameters. .. versionadded:: GMVER + :Parameters: + + as_class: `bool`, optional + If `True`, return the grid mapping as the equivalent + CF Class, for example cf.RotatedLatitudeLongitude, + rather than as a string corresponding to the value + of the 'grid_mapping_name' attribute, which is + the default. + :Returns: `dict` @@ -514,7 +513,10 @@ def get_grid_mappings(self): "grid_mapping_name", default=None ) if gm: - gms[cref_name] = gm + if as_class: + gms[cref_name] = _get_cf_grid_mapping_from_name(gm) + else: + gms[cref_name] = gm return gms def identity(self, default="", strict=False, relaxed=False, nc_only=False): diff --git a/cf/field.py b/cf/field.py index 912415e3c8..f113d9c955 100644 --- a/cf/field.py +++ b/cf/field.py @@ -15643,11 +15643,20 @@ def unlimited(self, *args): removed_at="4.0.0", ) # pragma: no cover - def get_grid_mappings(self): + def get_grid_mappings(self, as_class=False): """Returns coordinate conversions with grid mapping parameters. .. versionadded:: GMVER + :Parameters: + + as_class: `bool`, optional + If `True`, return the grid mapping as the equivalent + CF Class, for example cf.RotatedLatitudeLongitude, + rather than as a string corresponding to the value + of the 'grid_mapping_name' attribute, which is + the default. + :Returns: `dict` @@ -15659,9 +15668,11 @@ def get_grid_mappings(self): **Examples** >>> f.get_grid_mappings() - {"coordinatereference1": "rotated_latitude_longitude"} + {'coordinatereference1': "rotated_latitude_longitude"} + >>> f.get_grid_mappings(as_class=True) + {'coordinatereference1': cf.gridmappings.RotatedLatitudeLongitude} >>> g.get_grid_mappings() {} """ - return self.domain.get_grid_mappings() + return self.domain.get_grid_mappings(as_class=as_class) diff --git a/cf/test/test_Domain.py b/cf/test/test_Domain.py index 1fb5ee6b65..b3da35dfe1 100644 --- a/cf/test/test_Domain.py +++ b/cf/test/test_Domain.py @@ -301,6 +301,11 @@ def test_Domain_get_grid_mappings(self): 'coordinatereference1': 'rotated_latitude_longitude' } ) + self.assertEqual( + self.d.get_grid_mappings(as_class=True), { + 'coordinatereference1': cf.RotatedLatitudeLongitude + } + ) if __name__ == "__main__": diff --git a/cf/test/test_Field.py b/cf/test/test_Field.py index e210af48f2..69a524a4c9 100644 --- a/cf/test/test_Field.py +++ b/cf/test/test_Field.py @@ -2619,12 +2619,15 @@ def test_Field_get_grid_mappings(self): self.f.get_grid_mappings(), {'coordinatereference1': 'rotated_latitude_longitude'} ) + self.assertEqual( + self.f.get_grid_mappings(as_class=True), + {'coordinatereference1': cf.RotatedLatitudeLongitude} + ) self.assertEqual( self.f0.get_grid_mappings(), {} ) self.assertEqual( - self.f1.get_grid_mappings(), - {'coordinatereference1': 'rotated_latitude_longitude'} + self.f0.get_grid_mappings(as_class=True), {} ) From 0a7298e65767a870f1ecf491d4f682d231bd89fe Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Fri, 25 Aug 2023 11:34:48 +0100 Subject: [PATCH 49/97] Finalise docstrings for get_grid_mapping related funcs & methods --- cf/domain.py | 23 ++++++++++++++--------- cf/field.py | 19 +++++++++++-------- cf/gridmappings.py | 27 ++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/cf/domain.py b/cf/domain.py index b36d44818c..e411addf14 100644 --- a/cf/domain.py +++ b/cf/domain.py @@ -478,7 +478,7 @@ def get_data(self, default=ValueError(), _units=None, _fill_value=True): ) def get_grid_mappings(self, as_class=False): - """Returns coordinate conversions with grid mapping parameters. + """Returns coordinate conversions with their grid mappings. .. versionadded:: GMVER @@ -486,23 +486,28 @@ def get_grid_mappings(self, as_class=False): as_class: `bool`, optional If `True`, return the grid mapping as the equivalent - CF Class, for example cf.RotatedLatitudeLongitude, - rather than as a string corresponding to the value - of the 'grid_mapping_name' attribute, which is - the default. + CF Grid Mapping class, for example + cf.RotatedLatitudeLongitude, rather than as a string + corresponding to the value of the 'grid_mapping_name' + attribute, for example 'rotated_latitude_longitude'. + By default the 'grid_mapping_name' value is returned. :Returns: `dict` CoordinateConversion construct identifiers with - vaules of their 'grid_mapping_name' attributes, - for all CoordinateConversions of the domain that - have a 'grid_mapping_name' parameter defined. + values of their 'grid_mapping_name' attribute, + or corresponding CF Grid Mapping class if + as_class is `True`, for all CoordinateConversions + of the domain that have a 'grid_mapping_name' + parameter defined. **Examples** >>> f.get_grid_mappings() - {"coordinatereference1": "rotated_latitude_longitude"} + {'coordinatereference1': "rotated_latitude_longitude"} + >>> f.get_grid_mappings(as_class=True) + {'coordinatereference1': cf.gridmappings.RotatedLatitudeLongitude} >>> g.get_grid_mappings() {} diff --git a/cf/field.py b/cf/field.py index f113d9c955..b4d33d3b55 100644 --- a/cf/field.py +++ b/cf/field.py @@ -15644,7 +15644,7 @@ def unlimited(self, *args): ) # pragma: no cover def get_grid_mappings(self, as_class=False): - """Returns coordinate conversions with grid mapping parameters. + """Returns coordinate conversions with their grid mappings. .. versionadded:: GMVER @@ -15652,18 +15652,21 @@ def get_grid_mappings(self, as_class=False): as_class: `bool`, optional If `True`, return the grid mapping as the equivalent - CF Class, for example cf.RotatedLatitudeLongitude, - rather than as a string corresponding to the value - of the 'grid_mapping_name' attribute, which is - the default. + CF Grid Mapping class, for example + cf.RotatedLatitudeLongitude, rather than as a string + corresponding to the value of the 'grid_mapping_name' + attribute, for example 'rotated_latitude_longitude'. + By default the 'grid_mapping_name' value is returned. :Returns: `dict` CoordinateConversion construct identifiers with - vaules of their 'grid_mapping_name' attributes, - for all CoordinateConversions of the domain that - have a 'grid_mapping_name' parameter defined. + values of their 'grid_mapping_name' attribute, + or corresponding CF Grid Mapping class if + as_class is `True`, for all CoordinateConversions + of the domain that have a 'grid_mapping_name' + parameter defined. **Examples** diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 68731b8d47..43e3975a85 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -1801,7 +1801,32 @@ def get_proj_string(self): def _get_cf_grid_mapping_from_name(gm_name): - """TODO.""" + """Return the CF Grid Mapping class for a 'grid_mapping_name'. + + .. versionadded:: GMVER + + :Parameters: + + gm_name: `str` + The value of the 'grid_mapping_name' attribute + to convert to the equivalent CF Grid Mapping class. + + :Returns: + + `cf.GridMapping` + The CF Grid Mapping class corresponding to + the input 'grid_mapping_name' attribute. + + **Examples** + + >>> cf._get_cf_grid_mapping_from_name("vertical_perspective") + cf.gridmappings.VerticalPerspective + + >>> cf._get_cf_grid_mapping_from_name("lambert_conformal_conic") + cf.gridmappings.LambertConformalConic + + + """ cf_supported_gm_names = { gm.grid_mapping_name: gm for gm in _all_concrete_grid_mappings } From 70b4dce8ced08d8481d1d51e5b0d2483b3a9f74e Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Mon, 28 Aug 2023 13:35:30 +0100 Subject: [PATCH 50/97] Update test_gridmappings module docstrings --- cf/test/test_gridmappings.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 60f20fab92..3970a035b5 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -31,12 +31,14 @@ class GridMappingsTest(unittest.TestCase): - """TODO.""" + """Unit test for the GridMapping class and any derived classes.""" # Of the example fields, only 1, 6 and 7 have any coordinate references # with a coordinate conversion, hence use these to test, plus 0 as an # example case of a field without a coordinate reference at all. f = cf.example_fields() + # TODOGM, could do with a new example field or two with a grid mapping + # other than those two below, for diversity and testing on etc. f0 = f[0] # No coordinate reference f1 = f[1] # 2, with grid mappings of [None, 'rotated_latitude_longitude'] f6 = f[6] # 1, with grid mapping of ['latitude_longitude'] @@ -94,7 +96,7 @@ class GridMappingsTest(unittest.TestCase): # @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping__repr__str__(self): - """TODO.""" + """Test all means of GridMapping inspection..""" for cls in cf._all_concrete_grid_mappings: if cls.__name__ not in all_concrete_grid_mappings_req_args: g = cls() @@ -107,7 +109,7 @@ def test_grid_mapping__repr__str__(self): str(g) def test_grid_mapping__get_cf_grid_mapping_from_name(self): - """TODO.""" + """Test the '_get_cf_grid_mapping_from_name' function.""" for gm_name, cf_gm_class in { "vertical_perspective": cf.VerticalPerspective, "oblique_mercator": cf.ObliqueMercator, From de888be4426fba4e058da6ea6aaac3a6490176b1 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Tue, 29 Aug 2023 11:37:37 +0100 Subject: [PATCH 51/97] Extend to finalise test of CF->PROJ data --- cf/gridmappings.py | 6 ++++++ cf/test/test_gridmappings.py | 9 +++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 43e3975a85..5eae8f17ca 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -5,8 +5,10 @@ from pyproj import CRS from .data import Data +from .data.utils import is_numeric_dtype from .units import Units + PROJ_PREFIX = "+proj" ALL_GRID_MAPPING_ATTR_NAMES = { "grid_mapping_name", @@ -230,6 +232,10 @@ def convert_cf_angular_data_to_proj(data): raise ValueError( f"Input cf.Data must have size 1, got size: {data.size}" ) + if not is_numeric_dtype(data): + raise TypeError( + f"Input cf.Data must have numeric data type, got: {data.dtype}" + ) units = data.Units if not units: diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 3970a035b5..6645df529b 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -179,6 +179,7 @@ def test_grid_mapping_convert_cf_angular_data_to_proj(self): (cf.Data(45, units="degrees_east"), "45"), (cf.Data(45, units="degrees_E"), "45"), (cf.Data(45, units="degreeE"), "45"), + (cf.Data(45, units="degree"), "45"), # Check negative (cf.Data(-0.1, units="degrees"), "-0.1"), (cf.Data(-10, units="degrees"), "-10"), @@ -188,8 +189,8 @@ def test_grid_mapping_convert_cf_angular_data_to_proj(self): # Check radians units cases and >180 (cf.Data(190, units="radians"), "190R"), (cf.Data(190.0, units="radians"), "190.0R"), - # Check TODO - # TODO + # Check flot with superfluous 0 + (cf.Data(120.100, units="degrees"), "120.1"), ]: _input, correct_output = input_with_correct_output p = cf.convert_cf_angular_data_to_proj(_input) @@ -200,9 +201,13 @@ def test_grid_mapping_convert_cf_angular_data_to_proj(self): cf.Data([1, 2, 3]), # not singular (size 1) cf.Data(45), # no units cf.Data(45, "m"), # non-angular units + cf.Data(2, "elephants") # bad/non-CF units ]: with self.assertRaises(ValueError): cf.convert_cf_angular_data_to_proj(bad_input) + with self.assertRaises(TypeError): + # Non-numeric value + cf.convert_cf_angular_data_to_proj(cf.Data("N", "radians")) # Note that 'convert_cf_angular_data_to_proj' and # 'convert_proj_angular_data_to_cf' are not strict inverse From 341e4c655af7020cab742627653f62cedb81c67b Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Tue, 29 Aug 2023 16:29:20 +0100 Subject: [PATCH 52/97] Describe ALL_GRID_MAPPING_ATTR_NAMES ready for type checking --- cf/gridmappings.py | 77 ++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 5eae8f17ca..babecdae04 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -10,50 +10,61 @@ PROJ_PREFIX = "+proj" + +# Grid Mapping valid attribute names. Taken from the list given in +# 'Table F.1. Grid Mapping Attributes' in Appendix F: Grid Mappings' +# of the Conventions document. +# +# Values indicate if attribute values are expected to be numeric (instead +# of string) for the given attribute key (the table defines this via N, S). ALL_GRID_MAPPING_ATTR_NAMES = { - "grid_mapping_name", + "grid_mapping_name": False, # *Those which describe the ellipsoid and prime meridian:* - "earth_radius", # -------------------- PROJ '+R' value - "inverse_flattening", # -------------- PROJ '+rf' value - "longitude_of_prime_meridian", - "prime_meridian_name", # ------------- PROJ '+pm' value - "reference_ellipsoid_name", # -------- PROJ '+ellps' value - "semi_major_axis", # ----------------- PROJ '+a' value - "semi_minor_axis", # ----------------- PROJ '+b' value + "earth_radius": True, + "inverse_flattening": True, + "longitude_of_prime_meridian": True, + "prime_meridian_name": False, + "reference_ellipsoid_name": False, + "semi_major_axis": True, + "semi_minor_axis": True, # *Specific/applicable to only given grid mapping(s):* # ...projection origin related: - "longitude_of_projection_origin", # -- PROJ '+lon_0' value - "latitude_of_projection_origin", # --- PROJ '+lat_0' value - "scale_factor_at_projection_origin", # PROJ '+k_0' value + "longitude_of_projection_origin": True, + "latitude_of_projection_origin": True, + "scale_factor_at_projection_origin": True, # ...false-Xings: - "false_easting", # ------------------- PROJ '+x_0' value - "false_northing", # ------------------ PROJ '+y_0' value + "false_easting": True, + "false_northing": True, # ...angle axis related: - "sweep_angle_axis", # ---------------- PROJ '+sweep' value - "fixed_angle_axis", + "sweep_angle_axis": False, + "fixed_angle_axis": False, # ...central meridian related: - "longitude_of_central_meridian", - "scale_factor_at_central_meridian", + "longitude_of_central_meridian": True, + "scale_factor_at_central_meridian": True, # ...pole coordinates related: - "grid_north_pole_latitude", - "grid_north_pole_longitude", - "north_pole_grid_longitude", + "grid_north_pole_latitude": True, + "grid_north_pole_longitude": True, + "north_pole_grid_longitude": True, # ...other: - "standard_parallel", # --------------- PROJ ['+lat_1', '+lat_2'] values - "perspective_point_height", # -------- PROJ '+h' value - "azimuth_of_central_line", # --------- PROJ '+alpha' (ignore '+gamma') - "straight_vertical_longitude_from_pole", + "standard_parallel": True, + "perspective_point_height": True, + "azimuth_of_central_line": True, + "straight_vertical_longitude_from_pole": True, # *Other, not needed for a specific grid mapping but also listed # in 'Table F.1. Grid Mapping Attributes':* - "crs_wkt", # ------------------------- PROJ 'crs_wkt' value - "geographic_crs_name", - "geoid_name", - "geopotential_datum_name", - "horizontal_datum_name", - "projected_crs_name", - "towgs84", # ------------------------- PROJ '+towgs84' value + "crs_wkt": False, + "geographic_crs_name": False, + "geoid_name": False, + "geopotential_datum_name": False, + "horizontal_datum_name": False, + "projected_crs_name": False, + "towgs84": True, } -GRID_MAPPING_ATTR_TO_PROJ_STRING_COMP = { + +# Indicates what PROJ string ID component(s) refer(s) to the grid mapping +# attribute in question when '+' is added as a suffix, for example +# 'earth_radius' is '+R' and 'inverse_flattening' is '+rf' +GRID_MAPPING_ATTR_TO_PROJ_STRING_COMP_VALUES = { "earth_radius": "R", "inverse_flattening": "rf", "prime_meridian_name": "pm", @@ -68,7 +79,7 @@ "sweep_angle_axis": "sweep", "standard_parallel": ("lat_1", "lat_2"), "perspective_point_height": "h", - "azimuth_of_central_line": "alpha", + "azimuth_of_central_line": ("alpha", "gamma"), "crs_wkt": "crs_wkt", "towgs84": "towgs84", } From b0991e547c99fa5376e5bcbc3b38e9cabd9a40eb Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Wed, 30 Aug 2023 13:59:07 +0100 Subject: [PATCH 53/97] Update constants for gm_attrs and integrate w/ constants module --- cf/constants.py | 110 ++++++++++++++++++++++++++++++++++++++++----- cf/gridmappings.py | 79 +++----------------------------- 2 files changed, 105 insertions(+), 84 deletions(-) diff --git a/cf/constants.py b/cf/constants.py index ea335f63ee..3e0805a9e0 100644 --- a/cf/constants.py +++ b/cf/constants.py @@ -72,26 +72,116 @@ _stash2standard_name = {} + + + # --------------------------------------------------------------------- -# Coordinate reference constants TODO: turn these into functions +# Coordinate reference and grid mapping related constants # --------------------------------------------------------------------- +# Grid Mapping valid attribute names. Taken from the list given in +# 'Table F.1. Grid Mapping Attributes' in Appendix F: Grid Mappings' +# of the Conventions document. +# +# Values indicate if attribute values are expected to be numeric (instead +# of string) for the given attribute key (the table defines this via N, S). +# For any value where this is True, canonical units must be provided +# in the 'cr_canonical_units' dict below. +cr_gm_valid_attr_names_are_numeric = { + "grid_mapping_name": False, + # *Those which describe the ellipsoid and prime meridian:* + "earth_radius": True, + "inverse_flattening": True, + "longitude_of_prime_meridian": True, + "prime_meridian_name": False, + "reference_ellipsoid_name": False, + "semi_major_axis": True, + "semi_minor_axis": True, + # *Specific/applicable to only given grid mapping(s):* + # ...projection origin related: + "longitude_of_projection_origin": True, + "latitude_of_projection_origin": True, + "scale_factor_at_projection_origin": True, + # ...false-Xings: + "false_easting": True, + "false_northing": True, + # ...angle axis related: + "sweep_angle_axis": False, + "fixed_angle_axis": False, + # ...central meridian related: + "longitude_of_central_meridian": True, + "scale_factor_at_central_meridian": True, + # ...pole coordinates related: + "grid_north_pole_latitude": True, + "grid_north_pole_longitude": True, + "north_pole_grid_longitude": True, + # ...other: + "standard_parallel": True, + "perspective_point_height": True, + "azimuth_of_central_line": True, + "straight_vertical_longitude_from_pole": True, + # *Other, not needed for a specific grid mapping but also listed + # in 'Table F.1. Grid Mapping Attributes':* + "crs_wkt": False, + "geographic_crs_name": False, + "geoid_name": False, + "geopotential_datum_name": False, + "horizontal_datum_name": False, + "projected_crs_name": False, + "towgs84": True, +} cr_canonical_units = { + # *Those which describe the ellipsoid and prime meridian:* "earth_radius": Units("m"), - "grid_north_pole_latitude": Units("degrees_north"), - "grid_north_pole_longitude": Units("degrees_east"), "inverse_flattening": Units("1"), - "latitude_of_projection_origin": Units("degrees_north"), - "longitude_of_central_meridian": Units("degrees_east"), "longitude_of_prime_meridian": Units("degrees_east"), - "longitude_of_projection_origin": Units("degrees_east"), - "north_pole_grid_longitude": Units("degrees"), - "perspective_point_height": Units("m"), - "scale_factor_at_central_meridian": Units("1"), - "scale_factor_at_projection_origin": Units("1"), "semi_major_axis": Units("m"), "semi_minor_axis": Units("m"), + # *Specific/applicable to only given grid mapping(s):* + # ...projection origin related: + "longitude_of_projection_origin": Units("degrees_east"), + "latitude_of_projection_origin": Units("degrees_north"), + "scale_factor_at_projection_origin": Units("1"), + # ...false-Xings: + # MISSING: false_easting + # MISSING false_northing + # ...central meridian related: + "longitude_of_central_meridian": Units("degrees_east"), + "scale_factor_at_central_meridian": Units("1"), + # ...pole coordinates related: + "grid_north_pole_latitude": Units("degrees_north"), + "grid_north_pole_longitude": Units("degrees_east"), + "north_pole_grid_longitude": Units("degrees"), + # ...other: "standard_parallel": Units("degrees_north"), + "perspective_point_height": Units("m"), + # MISSING: "azimuth_of_central_line" "straight_vertical_longitude_from_pole": Units("degrees_north"), + # *Other, not needed for a specific grid mapping but also listed + # in 'Table F.1. Grid Mapping Attributes':* + # MISSING: "towgs84" +} + +# Indicates what PROJ string ID component(s) refer(s) to the grid mapping +# attribute in question when '+' is added as a suffix, for example +# 'earth_radius' is '+R' and 'inverse_flattening' is '+rf' +cr_gm_attr_to_proj_string_comps = { + "earth_radius": "R", + "inverse_flattening": "rf", + "prime_meridian_name": "pm", + "reference_ellipsoid_name": "ellps", + "semi_major_axis": "a", + "semi_minor_axis": "b", + "longitude_of_projection_origin": "lon_0", + "latitude_of_projection_origin": "lat_0", + "scale_factor_at_projection_origin": "k_0", + "false_easting": "x_0", + "false_northing": "y_0", + "sweep_angle_axis": "sweep", + "standard_parallel": ("lat_1", "lat_2"), + "perspective_point_height": "h", + "azimuth_of_central_line": ("alpha", "gamma"), + "crs_wkt": "crs_wkt", + "towgs84": "towgs84", } cr_coordinates = { diff --git a/cf/gridmappings.py b/cf/gridmappings.py index babecdae04..a469183046 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -4,6 +4,10 @@ from pyproj import CRS +from .constants import ( + cr_gm_valid_attr_names_are_numeric, + cr_gm_attr_to_proj_string_comps, +) from .data import Data from .data.utils import is_numeric_dtype from .units import Units @@ -11,79 +15,6 @@ PROJ_PREFIX = "+proj" -# Grid Mapping valid attribute names. Taken from the list given in -# 'Table F.1. Grid Mapping Attributes' in Appendix F: Grid Mappings' -# of the Conventions document. -# -# Values indicate if attribute values are expected to be numeric (instead -# of string) for the given attribute key (the table defines this via N, S). -ALL_GRID_MAPPING_ATTR_NAMES = { - "grid_mapping_name": False, - # *Those which describe the ellipsoid and prime meridian:* - "earth_radius": True, - "inverse_flattening": True, - "longitude_of_prime_meridian": True, - "prime_meridian_name": False, - "reference_ellipsoid_name": False, - "semi_major_axis": True, - "semi_minor_axis": True, - # *Specific/applicable to only given grid mapping(s):* - # ...projection origin related: - "longitude_of_projection_origin": True, - "latitude_of_projection_origin": True, - "scale_factor_at_projection_origin": True, - # ...false-Xings: - "false_easting": True, - "false_northing": True, - # ...angle axis related: - "sweep_angle_axis": False, - "fixed_angle_axis": False, - # ...central meridian related: - "longitude_of_central_meridian": True, - "scale_factor_at_central_meridian": True, - # ...pole coordinates related: - "grid_north_pole_latitude": True, - "grid_north_pole_longitude": True, - "north_pole_grid_longitude": True, - # ...other: - "standard_parallel": True, - "perspective_point_height": True, - "azimuth_of_central_line": True, - "straight_vertical_longitude_from_pole": True, - # *Other, not needed for a specific grid mapping but also listed - # in 'Table F.1. Grid Mapping Attributes':* - "crs_wkt": False, - "geographic_crs_name": False, - "geoid_name": False, - "geopotential_datum_name": False, - "horizontal_datum_name": False, - "projected_crs_name": False, - "towgs84": True, -} - -# Indicates what PROJ string ID component(s) refer(s) to the grid mapping -# attribute in question when '+' is added as a suffix, for example -# 'earth_radius' is '+R' and 'inverse_flattening' is '+rf' -GRID_MAPPING_ATTR_TO_PROJ_STRING_COMP_VALUES = { - "earth_radius": "R", - "inverse_flattening": "rf", - "prime_meridian_name": "pm", - "reference_ellipsoid_name": "ellps", - "semi_major_axis": "a", - "semi_minor_axis": "b", - "longitude_of_projection_origin": "lon_0", - "latitude_of_projection_origin": "lat_0", - "scale_factor_at_projection_origin": "k_0", - "false_easting": "x_0", - "false_northing": "y_0", - "sweep_angle_axis": "sweep", - "standard_parallel": ("lat_1", "lat_2"), - "perspective_point_height": "h", - "azimuth_of_central_line": ("alpha", "gamma"), - "crs_wkt": "crs_wkt", - "towgs84": "towgs84", -} - """ Define this first since it provides the default for several parameters, @@ -403,7 +334,7 @@ def __init__( """ for kwarg in kwargs: - if kwarg not in ALL_GRID_MAPPING_ATTR_NAMES: + if kwarg not in cr_gm_valid_attr_names_are_numeric: raise ValueError( "Unrecognised map parameter provided for the " f"Grid Mapping: {kwarg}" From 2b61c77828b3b1ae65fcbef7b72c7a9b790c0454 Mon Sep 17 00:00:00 2001 From: Sadie Louise Bartholomew Date: Wed, 30 Aug 2023 17:12:07 +0100 Subject: [PATCH 54/97] Constants: CR and GM constants tidying and coordination --- cf/constants.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/cf/constants.py b/cf/constants.py index 3e0805a9e0..bf89512445 100644 --- a/cf/constants.py +++ b/cf/constants.py @@ -142,8 +142,8 @@ "latitude_of_projection_origin": Units("degrees_north"), "scale_factor_at_projection_origin": Units("1"), # ...false-Xings: - # MISSING: false_easting - # MISSING false_northing + "false_easting": Units("m"), + "false_northing": Units("m"), # ...central meridian related: "longitude_of_central_meridian": Units("degrees_east"), "scale_factor_at_central_meridian": Units("1"), @@ -154,11 +154,18 @@ # ...other: "standard_parallel": Units("degrees_north"), "perspective_point_height": Units("m"), - # MISSING: "azimuth_of_central_line" + "azimuth_of_central_line": Units("degrees"), "straight_vertical_longitude_from_pole": Units("degrees_north"), # *Other, not needed for a specific grid mapping but also listed # in 'Table F.1. Grid Mapping Attributes':* - # MISSING: "towgs84" + # TODOGM towgs84 is very complicated, with multiple components + # of translations, roations and scaling each with different units + # requirements, so we should probably sort this out later, see: + # https://proj.org/en/9.3/operations/transformations/ + # helmert.html#helmert-transform + # where translations are in meters, rotations in arc-seconds, and + # scaling is in parts per million. + "towgs84": None, } # Indicates what PROJ string ID component(s) refer(s) to the grid mapping From e024e7ae35bb52014faed5bc9f5148ec9d3af01c Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 30 Aug 2023 17:28:46 +0100 Subject: [PATCH 55/97] Update Grid Mapping parameter docs for CF unit inputs --- cf/gridmappings.py | 1133 ++++++++++++++++++++++++-------------------- 1 file changed, 621 insertions(+), 512 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index a469183046..ed907dea4a 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -4,15 +4,13 @@ from pyproj import CRS -from .constants import ( +from .constants import ( # cr_gm_attr_to_proj_string_comps, cr_gm_valid_attr_names_are_numeric, - cr_gm_attr_to_proj_string_comps, ) from .data import Data from .data.utils import is_numeric_dtype from .units import Units - PROJ_PREFIX = "+proj" @@ -52,12 +50,19 @@ def convert_proj_angular_data_to_cf(proj_data, context=None): """Convert a PROJ angular data component into CF Data with CF Units. - Note that PROJ units for latitude and longitude are in + PROJ units for latitude and longitude are in units of decimal degrees, where forming a string by adding a suffix character indicates alternative units of radians if the suffix is 'R' or 'r'. If a string, a suffix of 'd', 'D' or '°' confirm units of decimal degrees. + Note that `cf.convert_cf_angular_data_to_proj` and + this function are not strict inverse functions, + since the former will always convert to the *simplest* + way to specify the PROJ input, namely with no suffix for + degrees(_X) units and the 'R' suffix for radians, whereas + the input might have 'D' or 'r' etc. instead. + .. versionadded:: GMVER :Parameters: @@ -151,8 +156,18 @@ def convert_proj_angular_data_to_cf(proj_data, context=None): def convert_cf_angular_data_to_proj(data): """Convert singleton angular CF Data into a PROJ data component. - PROJ units for latitude and longitude are generally in units of - decimal degrees. + PROJ units for latitude and longitude are in + units of decimal degrees, where forming a string by adding + a suffix character indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. + + Note that this function and `convert_proj_angular_data_to_cf` + are not strict inverse functions, since this function + will always convert to the *simplest* + way to specify the PROJ input, namely with no suffix for + degrees(_X) units and the 'R' suffix for radians, whereas + the input might have 'D' or 'r' etc. instead. .. versionadded:: GMVER @@ -285,11 +300,12 @@ def __init__( .. note:: If used in conjunction with 'earth_radius', the 'earth_radius' parameter takes precedence. - inverse_flattening: number, optional + inverse_flattening: number or scalar `Data`, optional The reverse flattening of the ellipsoid (PROJ 'rf' value), :math:`\frac{1}{f}`, where f corresponds to the flattening value (PROJ 'f' value) for the - ellipsoid. Unitless. The default is 298.257223563. + ellipsoid. Unitless, so `Data` must be unitless. + The default is 298.257223563. prime_meridian_name: `str`, optional A predeclared name to define the prime meridian (PROJ @@ -303,30 +319,44 @@ def __init__( 'longitude_of_prime_meridian', this parameter takes precedence. - longitude_of_prime_meridian: `str or `None`, optional + longitude_of_prime_meridian: number or scalar `Data`, optional The longitude relative to Greenwich of the - prime meridian. The default is 0.0. + prime meridian. If provided as a number or `Data` without + units, the units are taken as 'degrees_east', else the + `Data` units are taken and must be angular and + compatible with longitude. The default is 0.0 + degrees_east. .. note:: If used in conjunction with 'prime_meridian_name', the 'prime_meridian_name' parameter takes precedence. - semi_major_axis: number or `None`, optional - The semi-major axis of the ellipsoid (PROJ 'a' value) - in units of meters. The default is 6378137.0. + semi_major_axis: number, scalar `Data` or `None`, optional + The semi-major axis of the ellipsoid (PROJ 'a' value), + in units of meters unless units are otherwise specified + via `Data` units, in which case they must be conformable + to meters and will be converted. The default is 6378137.0. - semi_minor_axis: number or `None`, optional + semi_minor_axis: number, scalar `Data` or `None`, optional The semi-minor axis of the ellipsoid (PROJ 'b' value) - in units of meters. The default is 6356752.314245179. + in units of meters unless units are otherwise specified + via `Data` units, in which case they must be conformable + to meters and will be converted. The default is + 6356752.314245179. - earth_radius: number or `None`, optional + earth_radius: number, scalar `Data` or `None`, optional The radius of the ellipsoid, if a sphere (PROJ 'R' value), - in units of meters. If the ellipsoid is not a sphere, + in units of meters unless units are otherwise specified + via `Data` units, in which case they must be conformable + to meters and will be converted. + + If the ellipsoid is not a sphere, then set as `None`, the default, to indicate that ellipsoid parameters such as the reference_ellipsoid_name or semi_major_axis and semi_minor_axis are being set, - since these take precendence. + since these will should be used to define a + non-spherical ellipsoid. .. note:: If used in conjunction with 'reference_ellipsoid_name', this parameter @@ -400,29 +430,33 @@ class AzimuthalGridMapping(GridMapping): :Parameters: - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. """ @@ -449,43 +483,48 @@ class ConicGridMapping(GridMapping): :Parameters: - standard_parallel: number, `str` or 2-`tuple` - The standard parallel values, either the first (PROJ - 'lat_1' value), the second (PROJ 'lat_2' value) or - both, given as a 2-tuple of numbers or strings corresponding to + standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to the first and then the second in order, where `None` - indicates that a value is not being specified for either. In - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. + indicates that a value is not being set for either. + + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. - The default is (0.0, 0.0), that is 0.0 decimal degrees + The default is (0.0, 0.0), that is 0.0 degrees_north for the first and second standard parallel values. - longitude_of_central_meridian: number or `str`, optional - The longitude of (natural) origin i.e. central meridian, in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. """ @@ -514,13 +553,19 @@ class CylindricalGridMapping(GridMapping): :Parameters: - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. """ @@ -552,10 +597,13 @@ class PerspectiveGridMapping(AzimuthalGridMapping): :Parameters: - perspective_point_height: number + perspective_point_height: number or scalar `Data` The height of the view point above the surface (PROJ 'h') value, for example the height of a satellite above - the Earth, in units of meters. + the Earth, in units of meters 'm'. If provided + as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. """ @@ -595,43 +643,48 @@ class AlbersEqualArea(ConicGridMapping): :Parameters: - standard_parallel: number, `str` or 2-`tuple`, optional - The standard parallel values, either the first (PROJ - 'lat_1' value), the second (PROJ 'lat_2' value) or - both, given as a 2-tuple of numbers or strings corresponding to + standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to the first and then the second in order, where `None` - indicates that a value is not being specified for either. In - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. + indicates that a value is not being set for either. - The default is (0.0, 0.0), that is 0.0 decimal degrees + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. + + The default is (0.0, 0.0), that is 0.0 degrees_north for the first and second standard parallel values. - longitude_of_central_meridian: number or `str`, optional - The longitude of (natural) origin i.e. central meridian, in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. """ @@ -663,29 +716,33 @@ class AzimuthalEquidistant(AzimuthalGridMapping): :Parameters: - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. """ @@ -717,34 +774,41 @@ class Geostationary(PerspectiveGridMapping): :Parameters: - perspective_point_height: number + perspective_point_height: number or scalar `Data` The height of the view point above the surface (PROJ 'h') value, for example the height of a satellite above - the Earth, in units of meters. - - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + the Earth, in units of meters 'm'. If provided + as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. sweep_angle_axis: `str`, optional Sweep angle axis of the viewing instrument, which indicates @@ -830,29 +894,33 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): :Parameters: - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. """ @@ -884,43 +952,48 @@ class LambertConformalConic(ConicGridMapping): :Parameters: - standard_parallel: number, `str` or 2-`tuple` - The standard parallel values, either the first (PROJ - 'lat_1' value), the second (PROJ 'lat_2' value) or - both, given as a 2-tuple of numbers or strings corresponding to + standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to the first and then the second in order, where `None` - indicates that a value is not being specified for either. In - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. + indicates that a value is not being set for either. + + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. - The default is (0.0, 0.0), that is 0.0 decimal degrees + The default is (0.0, 0.0), that is 0.0 degrees_north for the first and second standard parallel values. - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. """ @@ -952,40 +1025,45 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): :Parameters: - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being set for either. - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. - standard_parallel: number, `str` or 2-`tuple`, optional - The standard parallel values, either the first (PROJ - 'lat_1' value), the second (PROJ 'lat_2' value) or - both, given as a 2-tuple of numbers or strings corresponding to - the first and then the second in order, where `None` - indicates that a value is not being specified for either. In - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. - - The default is (0.0, None), that is 0.0 decimal degrees - for the first standard parallel value and nothing set for - the second. - - longitude_of_central_meridian: number or `str`, optional - The longitude of (natural) origin i.e. central meridian, in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - scale_factor_at_projection_origin: number, optional + The default is (0.0, 0.0), that is 0.0 degrees_north + for the first and second standard parallel values. + + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + scale_factor_at_projection_origin: number or scalar `Data`, optional The scale factor used in the projection (PROJ 'k_0' value). - It is unitless. The default is 1.0. + Unitless, so `Data` must be unitless. The default is 1.0. """ @@ -1034,40 +1112,45 @@ class Mercator(CylindricalGridMapping): :Parameters: - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being set for either. - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. - standard_parallel: number, `str` or 2-`tuple`, optional - The standard parallel values, either the first (PROJ - 'lat_1' value), the second (PROJ 'lat_2' value) or - both, given as a 2-tuple of numbers or strings corresponding to - the first and then the second in order, where `None` - indicates that a value is not being specified for either. In - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. - - The default is (0.0, None), that is 0.0 decimal degrees - for the first standard parallel value and nothing set for - the second. - - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - scale_factor_at_projection_origin: number, optional + The default is (0.0, 0.0), that is 0.0 degrees_north + for the first and second standard parallel values. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + scale_factor_at_projection_origin: number or scalar `Data`, optional The scale factor used in the projection (PROJ 'k_0' value). - It is unitless. The default is 1.0. + Unitless, so `Data` must be unitless. The default is 1.0. """ @@ -1116,43 +1199,45 @@ class ObliqueMercator(CylindricalGridMapping): :Parameters: - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. - - azimuth_of_central_line: number or `str`, optional + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + azimuth_of_central_line: number or scalar `Data`, optional The azimuth i.e. tilt angle of the centerline clockwise from north at the center point of the line (PROJ 'alpha' - value), in units of decimal degrees, where - forming a string by adding a suffix character - indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - scale_factor_at_projection_origin: number, optional + value). If provided as a number or `Data` without units, + the units are taken as 'degrees', else the `Data` + units are taken and must be angular and compatible. + The default is 0.0 degrees. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + scale_factor_at_projection_origin: number or scalar `Data`, optional The scale factor used in the projection (PROJ 'k_0' value). - It is unitless. The default is 1.0. + Unitless, so `Data` must be unitless. The default is 1.0. """ @@ -1203,29 +1288,33 @@ class Orthographic(AzimuthalGridMapping): :Parameters: - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. """ @@ -1257,55 +1346,59 @@ class PolarStereographic(AzimuthalGridMapping): :Parameters: - straight_vertical_longitude_from_pole: number or `str`, optional + straight_vertical_longitude_from_pole: number or scalar `Data`, optional The longitude of (natural) origin i.e. central meridian, - oriented straight up from the North or South Pole, in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - scale_factor_at_projection_origin: number, optional + oriented straight up from the North or South Pole. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + scale_factor_at_projection_origin: number or scalar `Data`, optional The scale factor used in the projection (PROJ 'k_0' value). - It is unitless. The default is 1.0. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. - - standard_parallel: number, `str` or 2-`tuple`, optional - The standard parallel values, either the first (PROJ - 'lat_1' value), the second (PROJ 'lat_2' value) or - both, given as a 2-tuple of numbers or strings corresponding to + Unitless, so `Data` must be unitless. The default is 1.0. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to the first and then the second in order, where `None` - indicates that a value is not being specified for either. In - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. + indicates that a value is not being set for either. + + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. - The default is (0.0, 0.0), that is 0.0 decimal degrees + The default is (0.0, 0.0), that is 0.0 degrees_north for the first and second standard parallel values. """ @@ -1373,29 +1466,25 @@ class RotatedLatitudeLongitude(LatLonGridMapping): :Parameters: - grid_north_pole_latitude: number or `str` + grid_north_pole_latitude: number or scalar `Data` Latitude of the North pole of the unrotated source CRS, - expressed in the rotated geographic CRS, in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. + expressed in the rotated geographic CRS. + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. - grid_north_pole_longitude: number or `str` + grid_north_pole_longitude: number or scalar `Data` Longitude of the North pole of the unrotated source CRS, - expressed in the rotated geographic CRS, in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. - - north_pole_grid_longitude: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. + expressed in the rotated geographic CRS. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + + north_pole_grid_longitude: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. """ @@ -1464,21 +1553,26 @@ class Sinusoidal(GridMapping): :Parameters: - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. """ @@ -1523,33 +1617,37 @@ class Stereographic(AzimuthalGridMapping): :Parameters: - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. - - scale_factor_at_projection_origin: number, optional + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + scale_factor_at_projection_origin: number or scalar `Data`, optional The scale factor used in the projection (PROJ 'k_0' value). - It is unitless. The default is 1.0. + Unitless, so `Data` must be unitless. The default is 1.0. """ @@ -1602,33 +1700,37 @@ class TransverseMercator(CylindricalGridMapping): :Parameters: - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. - - scale_factor_at_central_meridian: number, optional - The scale factor at (natural) origin i.e. central meridian. - It is unitless. The default is 1.0. - - longitude_of_central_meridian: number or `str`, optional - The longitude of (natural) origin i.e. central meridian, in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + scale_factor_at_projection_origin: number or scalar `Data`, optional + The scale factor used in the projection (PROJ 'k_0' value). + Unitless, so `Data` must be unitless. The default is 1.0. + + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. """ @@ -1677,34 +1779,41 @@ class VerticalPerspective(PerspectiveGridMapping): :Parameters: - perspective_point_height: number + perspective_point_height: number or scalar `Data` The height of the view point above the surface (PROJ 'h') value, for example the height of a satellite above - the Earth, in units of meters. - - longitude_of_projection_origin: number or `str`, optional - The longitude of projection center (PROJ 'lon_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - latitude_of_projection_origin: number or `str`, optional - The latitude of projection center (PROJ 'lat_0' value), in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. The default - is 0.0 decimal degrees. - - false_easting: number, optional - The false easting (PROJ 'x_0') value, in units of metres. - The default is 0.0. - - false_northing: number, optional - The false northing (PROJ 'y_0') value, in units of metres. - The default is 0.0. + the Earth, in units of meters 'm'. If provided + as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. """ @@ -1751,27 +1860,27 @@ def get_proj_string(self): def _get_cf_grid_mapping_from_name(gm_name): """Return the CF Grid Mapping class for a 'grid_mapping_name'. - .. versionadded:: GMVER + .. versionadded:: GMVER - :Parameters: + :Parameters: - gm_name: `str` - The value of the 'grid_mapping_name' attribute - to convert to the equivalent CF Grid Mapping class. + gm_name: `str` + The value of the 'grid_mapping_name' attribute + to convert to the equivalent CF Grid Mapping class. - :Returns: + :Returns: - `cf.GridMapping` - The CF Grid Mapping class corresponding to - the input 'grid_mapping_name' attribute. + `cf.GridMapping` + The CF Grid Mapping class corresponding to + the input 'grid_mapping_name' attribute. - **Examples** + **Examples** - >>> cf._get_cf_grid_mapping_from_name("vertical_perspective") - cf.gridmappings.VerticalPerspective + >>> cf._get_cf_grid_mapping_from_name("vertical_perspective") + cf.gridmappings.VerticalPerspective - >>> cf._get_cf_grid_mapping_from_name("lambert_conformal_conic") - cf.gridmappings.LambertConformalConic + >>> cf._get_cf_grid_mapping_from_name("lambert_conformal_conic") + cf.gridmappings.LambertConformalConic """ From 73b038d924ce5e8e936a0452318226ebf52deed3 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 30 Aug 2023 17:53:12 +0100 Subject: [PATCH 56/97] Fix old canonical units for straight_vertical_longitude_from_pole --- cf/constants.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cf/constants.py b/cf/constants.py index bf89512445..93b86dee59 100644 --- a/cf/constants.py +++ b/cf/constants.py @@ -73,8 +73,6 @@ _stash2standard_name = {} - - # --------------------------------------------------------------------- # Coordinate reference and grid mapping related constants # --------------------------------------------------------------------- @@ -155,7 +153,9 @@ "standard_parallel": Units("degrees_north"), "perspective_point_height": Units("m"), "azimuth_of_central_line": Units("degrees"), - "straight_vertical_longitude_from_pole": Units("degrees_north"), + # TODOGM check the below is correct, was 'degrees_north' before but it + # is a longitude so surely that was wrong?: + "straight_vertical_longitude_from_pole": Units("degrees_east"), # *Other, not needed for a specific grid mapping but also listed # in 'Table F.1. Grid Mapping Attributes':* # TODOGM towgs84 is very complicated, with multiple components From 723fed35c2b00e55cd9ac9673e3ac6bd5cf6bfcb Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 30 Aug 2023 18:07:04 +0100 Subject: [PATCH 57/97] Update optional nature of standard_parallel parameter kwarg --- cf/gridmappings.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index ed907dea4a..650ae9afab 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -655,9 +655,6 @@ class AlbersEqualArea(ConicGridMapping): the `Data` units are taken and must be angular and compatible with latitude. - The default is (0.0, 0.0), that is 0.0 degrees_north - for the first and second standard parallel values. - longitude_of_central_meridian: number or scalar `Data`, optional The longitude of (natural) origin i.e. central meridian. If provided as a number or `Data` without units, the units @@ -1039,7 +1036,8 @@ class LambertCylindricalEqualArea(CylindricalGridMapping): taken and must be compatible with distance. The default is 0.0 metres. - standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + standard_parallel: 2-`tuple` of number or scalar `Data` + or `None`, optional The standard parallel value(s): the first (PROJ 'lat_1' value) and/or the second (PROJ 'lat_2' value), given as a 2-tuple of numbers or strings corresponding to @@ -1074,7 +1072,7 @@ def __init__( self, false_easting=0.0, false_northing=0.0, - standard_parallel=(0.0, None), + standard_parallel=(0.0, 0.0), scale_factor_at_projection_origin=1.0, longitude_of_central_meridian=0.0, **kwargs, @@ -1126,7 +1124,8 @@ class Mercator(CylindricalGridMapping): taken and must be compatible with distance. The default is 0.0 metres. - standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + standard_parallel: 2-`tuple` of number or scalar `Data` + or `None`, optional The standard parallel value(s): the first (PROJ 'lat_1' value) and/or the second (PROJ 'lat_2' value), given as a 2-tuple of numbers or strings corresponding to @@ -1161,7 +1160,7 @@ def __init__( self, false_easting=0.0, false_northing=0.0, - standard_parallel=(0.0, None), + standard_parallel=(0.0, 0.0), longitude_of_projection_origin=0.0, scale_factor_at_projection_origin=1.0, **kwargs, @@ -1386,7 +1385,8 @@ class PolarStereographic(AzimuthalGridMapping): taken and must be compatible with distance. The default is 0.0 metres. - standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + standard_parallel: 2-`tuple` of number or scalar `Data` + or `None`, optional The standard parallel value(s): the first (PROJ 'lat_1' value) and/or the second (PROJ 'lat_2' value), given as a 2-tuple of numbers or strings corresponding to From d2cf1b1cc5d2274c206ea4a6b03fd6113daa9050 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 30 Aug 2023 19:08:17 +0100 Subject: [PATCH 58/97] Update GM comparison logic to use CRS not proj string (order dep.) --- cf/gridmappings.py | 100 ++++++--------------------------------------- 1 file changed, 12 insertions(+), 88 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 650ae9afab..ddbde49a0a 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -44,8 +44,6 @@ """ WGS1984_CF_ATTR_DEFAULTS = CRS.from_proj4("+proj=merc").to_cf() -DUMMY_PARAMS = {"a": "b", "c": 0.0} # TODOPARAMETERS, drop this - def convert_proj_angular_data_to_cf(proj_data, context=None): """Convert a PROJ angular data component into CF Data with CF Units. @@ -411,16 +409,22 @@ def __str__(self): def __eq__(self, other): """The rich comparison operator ``==``.""" - return self.get_proj_string() == other.get_proj_string() + return self.get_proj_crs() == other.get_proj_crs() def __hash__(self, other): - """The rich comparison operator ``==``.""" - return hash(self.get_proj_string()) + """The built-in function `hash`, x.__hash__() <==> hash(x).""" + return hash(self.get_proj_crs()) - @abstractmethod - def get_proj_string(self): + def get_proj_string(self, params=None): """The value of the PROJ proj-string defining the projection.""" - pass + # TODO enable parameter input and sync'ing + if not params: + params = "" + return f"{PROJ_PREFIX}={self.proj_id}{params}" + + def get_proj_crs(self): + """TODO.""" + return CRS.from_proj4(self.get_proj_string()) class AzimuthalGridMapping(GridMapping): @@ -688,11 +692,6 @@ class AlbersEqualArea(ConicGridMapping): grid_mapping_name = "albers_conical_equal_area" proj_id = "aea" - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class AzimuthalEquidistant(AzimuthalGridMapping): """The Azimuthal Equidistant grid mapping. @@ -746,11 +745,6 @@ class AzimuthalEquidistant(AzimuthalGridMapping): grid_mapping_name = "azimuthal_equidistant" proj_id = "aeqd" - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class Geostationary(PerspectiveGridMapping): """The Geostationary Satellite View grid mapping. @@ -866,11 +860,6 @@ def __init__( self.sweep_angle_axis = sweep_angle_axis.lower() self.fixed_angle_axis = fixed_angle_axis.lower() - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class LambertAzimuthalEqualArea(AzimuthalGridMapping): """The Lambert Azimuthal Equal Area grid mapping. @@ -924,11 +913,6 @@ class LambertAzimuthalEqualArea(AzimuthalGridMapping): grid_mapping_name = "lambert_azimuthal_equal_area" proj_id = "laea" - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class LambertConformalConic(ConicGridMapping): """The Lambert Conformal Conic grid mapping. @@ -997,11 +981,6 @@ class LambertConformalConic(ConicGridMapping): grid_mapping_name = "lambert_conformal_conic" proj_id = "lcc" - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class LambertCylindricalEqualArea(CylindricalGridMapping): """The Equal Area Cylindrical grid mapping. @@ -1085,11 +1064,6 @@ def __init__( scale_factor_at_projection_origin ) - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class Mercator(CylindricalGridMapping): """The Mercator grid mapping. @@ -1173,11 +1147,6 @@ def __init__( scale_factor_at_projection_origin ) - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class ObliqueMercator(CylindricalGridMapping): """The Oblique Mercator grid mapping. @@ -1262,11 +1231,6 @@ def __init__( scale_factor_at_projection_origin ) - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class Orthographic(AzimuthalGridMapping): """The Orthographic grid mapping. @@ -1320,11 +1284,6 @@ class Orthographic(AzimuthalGridMapping): grid_mapping_name = "orthographic" proj_id = "ortho" - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class PolarStereographic(AzimuthalGridMapping): """The Universal Polar Stereographic grid mapping. @@ -1445,11 +1404,6 @@ def __init__( scale_factor_at_projection_origin ) - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class RotatedLatitudeLongitude(LatLonGridMapping): """The Rotated Latitude-Longitude grid mapping. @@ -1504,11 +1458,6 @@ def __init__( self.grid_north_pole_longitude = grid_north_pole_longitude self.north_pole_grid_longitude = north_pole_grid_longitude - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class LatitudeLongitude(LatLonGridMapping): """The Latitude-Longitude grid mapping. @@ -1528,11 +1477,6 @@ class LatitudeLongitude(LatLonGridMapping): grid_mapping_name = "latitude_longitude" proj_id = "latlong" - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class Sinusoidal(GridMapping): """The Sinusoidal (Sanson-Flamsteed) grid mapping. @@ -1592,11 +1536,6 @@ def __init__( self.false_easting = false_easting self.false_northing = false_northing - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class Stereographic(AzimuthalGridMapping): """The Stereographic grid mapping. @@ -1675,11 +1614,6 @@ def __init__( scale_factor_at_projection_origin ) - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class TransverseMercator(CylindricalGridMapping): """The Transverse Mercator grid mapping. @@ -1754,11 +1688,6 @@ def __init__( self.longitude_of_central_meridian = longitude_of_central_meridian self.latitude_of_projection_origin = latitude_of_projection_origin - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - class VerticalPerspective(PerspectiveGridMapping): """The Vertical (or Near-sided) Perspective grid mapping. @@ -1820,11 +1749,6 @@ class VerticalPerspective(PerspectiveGridMapping): grid_mapping_name = "vertical_perspective" proj_id = "nsper" - def get_proj_string(self): - """The value of the PROJ proj-string defining the projection.""" - parameters = _make_proj_string_comp(DUMMY_PARAMS) # TODOPARAMETERS - return f"{PROJ_PREFIX}={self.proj_id}{parameters}" - # TODO move this definition elsewhere, having at end feels like an # anti-pattern... From b6c2a6632cb6b500a8aa05dc012f99bf53ce7bc1 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 1 Sep 2023 10:09:06 +0100 Subject: [PATCH 59/97] Extend test_gridmapping GM init testing + tidy --- cf/test/test_gridmappings.py | 44 ++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 6645df529b..611f4ae812 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -94,9 +94,23 @@ class GridMappingsTest(unittest.TestCase): "+units=m +no_defs" ) - # @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") + @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") + def test_grid_mapping__init__(self): + """Test GridMapping object initiation.""" + for cls in cf._all_concrete_grid_mappings: + if cls.__name__ not in all_concrete_grid_mappings_req_args: + cls() + + # Shouldn't be able to instantiate any abstract classes, since + # abstract methods grid_mapping_name and proj_id must be defined + # further down in the inheritance chain to enable concrete classes. + for cls in cf._all_abstract_grid_mappings: + with self.assertRaises(TypeError): + cls() + + @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping__repr__str__(self): - """Test all means of GridMapping inspection..""" + """Test all means of GridMapping inspection.""" for cls in cf._all_concrete_grid_mappings: if cls.__name__ not in all_concrete_grid_mappings_req_args: g = cls() @@ -108,6 +122,29 @@ def test_grid_mapping__repr__str__(self): repr(g) str(g) + g1 = cf.Mercator() + self.assertEqual(repr(g1), "") + self.assertEqual( + str(g1), "" + ) + + g2 = cf.Orthographic() + self.assertEqual(repr(g2), "") + self.assertEqual( + str(g2), "" + ) + + g3 = cf.Sinusoidal() + self.assertEqual(repr(g3), "") + self.assertEqual(str(g3), "") + + g4 = cf.Stereographic() + self.assertEqual(repr(g4), "") + self.assertEqual( + str(g4), "" + ) + + @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping__get_cf_grid_mapping_from_name(self): """Test the '_get_cf_grid_mapping_from_name' function.""" for gm_name, cf_gm_class in { @@ -201,7 +238,7 @@ def test_grid_mapping_convert_cf_angular_data_to_proj(self): cf.Data([1, 2, 3]), # not singular (size 1) cf.Data(45), # no units cf.Data(45, "m"), # non-angular units - cf.Data(2, "elephants") # bad/non-CF units + cf.Data(2, "elephants"), # bad/non-CF units ]: with self.assertRaises(ValueError): cf.convert_cf_angular_data_to_proj(bad_input) @@ -230,7 +267,6 @@ def test_grid_mapping_convert_cf_angular_data_to_proj(self): # be re-generated since degrees_X gets converted back to the # default degrees, so skip those in these test cases. if not p.endswith("R"): - a = cf.convert_proj_angular_data_to_cf(p, context="lat") p3 = cf.convert_cf_angular_data_to_proj( cf.convert_proj_angular_data_to_cf(p, context="lat") ) From 5c278b871716df7abfb3b44767c965d114ca6d68 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 1 Sep 2023 11:55:13 +0100 Subject: [PATCH 60/97] Add map parameter validation and supply of canonical units --- cf/gridmappings.py | 117 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 16 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index ddbde49a0a..2eb3a1b8c1 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -4,9 +4,7 @@ from pyproj import CRS -from .constants import ( # cr_gm_attr_to_proj_string_comps, - cr_gm_valid_attr_names_are_numeric, -) +from .constants import cr_canonical_units, cr_gm_valid_attr_names_are_numeric from .data import Data from .data.utils import is_numeric_dtype from .units import Units @@ -252,6 +250,82 @@ def _make_proj_string_comp(spec): return proj_string +def _validate_map_parameter(mp_name, mp_value): + """TODO + + :Parameters: + + mp_name: `str` + TODO + + mp_value: TODO + TODO + + :Returns: + + `dict` + TODO + + """ + # 0. Check input parameters are valid CF GM map parameters, not + # for any case something unrecognised that will silently do nothing. + if mp_name not in cr_gm_valid_attr_names_are_numeric: + raise ValueError( + "Unrecognised map parameter provided for the " + f"Grid Mapping: {mp_name}" + ) + + # 1. If None, can return early: + if mp_value is None: # distinguish from 0 or 0.0 etc. + return mp_name, None + + # 2. Now ensure the type of the value is as expected. + expect_numeric = cr_gm_valid_attr_names_are_numeric[mp_name] + if expect_numeric: + if (isinstance(mp_value, Data) and not is_numeric_dtype(mp_value)) or ( + not isinstance(mp_value, (int, float)) + ): + raise TypeError( + f"Map parameter {mp_name} has an incompatible " + "data type, expected numeric or Data but got " + f"{type(mp_value)}" + ) + elif not expect_numeric and not isinstance(mp_value, str): + raise TypeError( + f"Map parameter {mp_name} has an incompatible " + "data type, expected a string but got " + f"{type(mp_value)}" + ) + + # 3. Finally ensure the units are valid and conformed to the + # canonical units, where numeric. + if expect_numeric: + canon_units = cr_canonical_units[mp_name] + if isinstance(mp_value, Data): + # In this case, is Data which may have units which aren't equal to + # the canonical ones, so may need to conform the value. + units = mp_value.Units + # The units must be checked and might need to be conformed + if not units.equivalent(canon_units): + raise ValueError( + f"Map parameter {mp_name} value has units that " + "are incompatible with the expected units of " + f"{canon_units}: {units}" + ) + elif not units.equals(canon_units): + conforming_value = Units.conform( + mp_value.array.item(), units, canon_units + ) + else: + conforming_value = mp_value + + # Return numeric value as Data with conformed value and canonical units + return mp_name, Data(conforming_value, units=canon_units) + else: + # Return string value + return mp_name, mp_value + + """Abstract classes for general Grid Mappings. Note that default arguments are based upon the PROJ defaults, which can @@ -361,22 +435,33 @@ def __init__( takes precedence. """ - for kwarg in kwargs: - if kwarg not in cr_gm_valid_attr_names_are_numeric: - raise ValueError( - "Unrecognised map parameter provided for the " - f"Grid Mapping: {kwarg}" - ) + # Validate the arbitary kwargs + for kwarg, value in kwargs.items(): + _validate_map_parameter(kwarg, value) # The attributes which describe the ellipsoid and prime meridian, # which may be included, when applicable, with any grid mapping. - self.earth_radius = earth_radius - self.inverse_flattening = inverse_flattening - self.longitude_of_prime_meridian = longitude_of_prime_meridian - self.prime_meridian_name = prime_meridian_name - self.reference_ellipsoid_name = reference_ellipsoid_name - self.semi_major_axis = semi_major_axis - self.semi_minor_axis = semi_minor_axis + self.earth_radius = _validate_map_parameter( + "earth_radius", earth_radius + ) + self.inverse_flattening = _validate_map_parameter( + "inverse_flattening", inverse_flattening + ) + self.longitude_of_prime_meridian = _validate_map_parameter( + "longitude_of_prime_meridian", longitude_of_prime_meridian + ) + self.prime_meridian_name = _validate_map_parameter( + "prime_meridian_name", prime_meridian_name + ) + self.reference_ellipsoid_name = _validate_map_parameter( + "reference_ellipsoid_name", reference_ellipsoid_name + ) + self.semi_major_axis = _validate_map_parameter( + "semi_major_axis", semi_major_axis + ) + self.semi_minor_axis = _validate_map_parameter( + "semi_minor_axis", semi_minor_axis + ) @property @classmethod From 5c192175d7e6049cebca855e947b0f00d88dfe55 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 1 Sep 2023 12:50:42 +0100 Subject: [PATCH 61/97] Fix map parameter validation for standard_parallel tuple --- cf/gridmappings.py | 171 ++++++++++++++++++++++++++++++++------------- 1 file changed, 121 insertions(+), 50 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 2eb3a1b8c1..d7f7cdaed3 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -277,7 +277,7 @@ def _validate_map_parameter(mp_name, mp_value): # 1. If None, can return early: if mp_value is None: # distinguish from 0 or 0.0 etc. - return mp_name, None + return None # 2. Now ensure the type of the value is as expected. expect_numeric = cr_gm_valid_attr_names_are_numeric[mp_name] @@ -320,10 +320,10 @@ def _validate_map_parameter(mp_name, mp_value): conforming_value = mp_value # Return numeric value as Data with conformed value and canonical units - return mp_name, Data(conforming_value, units=canon_units) + return Data(conforming_value, units=canon_units) else: # Return string value - return mp_name, mp_value + return mp_value """Abstract classes for general Grid Mappings. @@ -559,10 +559,18 @@ def __init__( ): super().__init__(**kwargs) - self.longitude_of_projection_origin = longitude_of_projection_origin - self.latitude_of_projection_origin = latitude_of_projection_origin - self.false_easting = false_easting - self.false_northing = false_northing + self.longitude_of_projection_origin = _validate_map_parameter( + "longitude_of_projection_origin", longitude_of_projection_origin + ) + self.latitude_of_projection_origin = _validate_map_parameter( + "latitude_of_projection_origin", latitude_of_projection_origin + ) + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) class ConicGridMapping(GridMapping): @@ -628,11 +636,22 @@ def __init__( ): super().__init__(**kwargs) - self.standard_parallel = standard_parallel - self.longitude_of_central_meridian = longitude_of_central_meridian - self.latitude_of_projection_origin = latitude_of_projection_origin - self.false_easting = false_easting - self.false_northing = false_northing + self.standard_parallel = ( + _validate_map_parameter("standard_parallel", standard_parallel[0]), + _validate_map_parameter("standard_parallel", standard_parallel[1]), + ) + self.longitude_of_central_meridian = _validate_map_parameter( + "longitude_of_central_meridian", longitude_of_central_meridian + ) + self.latitude_of_projection_origin = _validate_map_parameter( + "latitude_of_projection_origin", latitude_of_projection_origin + ) + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) class CylindricalGridMapping(GridMapping): @@ -661,8 +680,12 @@ class CylindricalGridMapping(GridMapping): def __init__(self, false_easting=0.0, false_northing=0.0, **kwargs): super().__init__(**kwargs) - self.false_easting = false_easting - self.false_northing = false_northing + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) class LatLonGridMapping(GridMapping): @@ -699,7 +722,9 @@ class PerspectiveGridMapping(AzimuthalGridMapping): def __init__(self, perspective_point_height, **kwargs): super().__init__(**kwargs) - self.perspective_point_height = perspective_point_height + self.perspective_point_height = _validate_map_parameter( + "perspective_point_height", perspective_point_height + ) """Concrete classes for all Grid Mappings supported by the CF Conventions. @@ -930,9 +955,17 @@ def __init__( **kwargs, ) + # Values "x" and "y" are not case-sensitive, so convert to lower-case + self.sweep_angle_axis = _validate_map_parameter( + "sweep_angle_axis", sweep_angle_axis + ).lower() + self.fixed_angle_axis = _validate_map_parameter( + "fixed_angle_axis", fixed_angle_axis + ).lower() + # sweep_angle_axis must be the opposite (of "x" and "y") to # fixed_angle_axis. - if (sweep_angle_axis.lower(), fixed_angle_axis.lower()) not in [ + if (self.sweep_angle_axis, self.fixed_angle_axis) not in [ ("x", "y"), ("y", "x"), ]: @@ -941,10 +974,6 @@ def __init__( "and 'y', to the fixed_angle_axis." ) - # Values "x" and "y" are not case-sensitive, so convert to lower-case - self.sweep_angle_axis = sweep_angle_axis.lower() - self.fixed_angle_axis = fixed_angle_axis.lower() - class LambertAzimuthalEqualArea(AzimuthalGridMapping): """The Lambert Azimuthal Equal Area grid mapping. @@ -1143,10 +1172,16 @@ def __init__( ): super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) - self.standard_parallel = standard_parallel - self.longitude_of_central_meridian = longitude_of_central_meridian - self.scale_factor_at_projection_origin = ( - scale_factor_at_projection_origin + self.standard_parallel = ( + _validate_map_parameter("standard_parallel", standard_parallel[0]), + _validate_map_parameter("standard_parallel", standard_parallel[1]), + ) + self.longitude_of_central_meridian = _validate_map_parameter( + "longitude_of_central_meridian", longitude_of_central_meridian + ) + self.scale_factor_at_projection_origin = _validate_map_parameter( + "scale_factor_at_projection_origin", + scale_factor_at_projection_origin, ) @@ -1226,10 +1261,16 @@ def __init__( ): super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) - self.standard_parallel = standard_parallel - self.longitude_of_projection_origin = longitude_of_projection_origin - self.scale_factor_at_projection_origin = ( - scale_factor_at_projection_origin + self.standard_parallel = ( + _validate_map_parameter("standard_parallel", standard_parallel[0]), + _validate_map_parameter("standard_parallel", standard_parallel[1]), + ) + self.longitude_of_projection_origin = _validate_map_parameter( + "longitude_of_projection_origin", longitude_of_projection_origin + ) + self.scale_factor_at_projection_origin = _validate_map_parameter( + "scale_factor_at_projection_origin", + scale_factor_at_projection_origin, ) @@ -1309,11 +1350,18 @@ def __init__( ): super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) - self.azimuth_of_central_line = azimuth_of_central_line - self.latitude_of_projection_origin = latitude_of_projection_origin - self.longitude_of_projection_origin = longitude_of_projection_origin - self.scale_factor_at_projection_origin = ( - scale_factor_at_projection_origin + self.azimuth_of_central_line = _validate_map_parameter( + "azimuth_of_central_line", azimuth_of_central_line + ) + self.latitude_of_projection_origin = _validate_map_parameter( + "latitude_of_projection_origin", latitude_of_projection_origin + ) + self.longitude_of_projection_origin = _validate_map_parameter( + "longitude_of_projection_origin", longitude_of_projection_origin + ) + self.scale_factor_at_projection_origin = _validate_map_parameter( + "scale_factor_at_projection_origin", + scale_factor_at_projection_origin, ) @@ -1481,12 +1529,17 @@ def __init__( "'straight_vertical_longitude_from_pole' can be set." ) - self.straight_vertical_longitude_from_pole = ( - straight_vertical_longitude_from_pole + self.straight_vertical_longitude_from_pole = _validate_map_parameter( + "straight_vertical_longitude_from_pole", + straight_vertical_longitude_from_pole, + ) + self.standard_parallel = ( + _validate_map_parameter("standard_parallel", standard_parallel[0]), + _validate_map_parameter("standard_parallel", standard_parallel[1]), ) - self.standard_parallel = standard_parallel - self.scale_factor_at_projection_origin = ( - scale_factor_at_projection_origin + self.scale_factor_at_projection_origin = _validate_map_parameter( + "scale_factor_at_projection_origin", + scale_factor_at_projection_origin, ) @@ -1539,9 +1592,15 @@ def __init__( ): super().__init__(**kwargs) - self.grid_north_pole_latitude = grid_north_pole_latitude - self.grid_north_pole_longitude = grid_north_pole_longitude - self.north_pole_grid_longitude = north_pole_grid_longitude + self.grid_north_pole_latitude = _validate_map_parameter( + "grid_north_pole_latitude", grid_north_pole_latitude + ) + self.grid_north_pole_longitude = _validate_map_parameter( + "grid_north_pole_longitude", grid_north_pole_longitude + ) + self.north_pole_grid_longitude = _validate_map_parameter( + "north_pole_grid_longitude", north_pole_grid_longitude + ) class LatitudeLongitude(LatLonGridMapping): @@ -1617,9 +1676,15 @@ def __init__( ): super().__init__(**kwargs) - self.longitude_of_projection_origin = longitude_of_projection_origin - self.false_easting = false_easting - self.false_northing = false_northing + self.longitude_of_projection_origin = _validate_map_parameter( + "longitude_of_projection_origin", longitude_of_projection_origin + ) + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) class Stereographic(AzimuthalGridMapping): @@ -1695,8 +1760,9 @@ def __init__( **kwargs, ) - self.scale_factor_at_projection_origin = ( - scale_factor_at_projection_origin + self.scale_factor_at_projection_origin = _validate_map_parameter( + "scale_factor_at_projection_origin", + scale_factor_at_projection_origin, ) @@ -1767,11 +1833,16 @@ def __init__( ): super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) - self.scale_factor_at_central_meridian = ( - scale_factor_at_central_meridian + self.scale_factor_at_central_meridian = _validate_map_parameter( + "scale_factor_at_central_meridian", + scale_factor_at_central_meridian, + ) + self.longitude_of_central_meridian = _validate_map_parameter( + "longitude_of_central_meridian", longitude_of_central_meridian + ) + self.latitude_of_projection_origin = _validate_map_parameter( + "latitude_of_projection_origin", latitude_of_projection_origin ) - self.longitude_of_central_meridian = longitude_of_central_meridian - self.latitude_of_projection_origin = latitude_of_projection_origin class VerticalPerspective(PerspectiveGridMapping): From 36758042dff943844f253557b9fd6160195b1945 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 1 Sep 2023 14:51:10 +0100 Subject: [PATCH 62/97] Fix test_grid_mapping__repr__str__ via new valid input --- cf/test/test_gridmappings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 611f4ae812..67016ea361 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -19,10 +19,10 @@ # These are those of the above which have required positional arguments all_concrete_grid_mappings_req_args = { - "AlbersEqualArea": {"standard_parallel": 0.0}, + "AlbersEqualArea": {"standard_parallel": (0.0, None)}, "Geostationary": {"perspective_point_height": 1000}, "VerticalPerspective": {"perspective_point_height": 1000}, - "LambertConformalConic": {"standard_parallel": 0.0}, + "LambertConformalConic": {"standard_parallel": (1.0, 1.0)}, "RotatedLatitudeLongitude": { "grid_north_pole_latitude": 0.0, "grid_north_pole_longitude": 0.0, @@ -118,7 +118,7 @@ def test_grid_mapping__repr__str__(self): example_minimal_args = all_concrete_grid_mappings_req_args[ cls.__name__ ] - g = cls(*example_minimal_args) + g = cls(**example_minimal_args) repr(g) str(g) From abb22498705a464c292d5823a89b2c098902e538 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 1 Sep 2023 15:44:46 +0100 Subject: [PATCH 63/97] Add minimal test for map parameter validation and conformance --- cf/test/test_gridmappings.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 67016ea361..8dfc00fe11 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -144,6 +144,31 @@ def test_grid_mapping__repr__str__(self): str(g4), "" ) + @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") + def test_grid_mapping_map_parameter_validation(self): + """Test the validation of map parameters to Grid Mapping classes.""" + g1 = cf.Mercator( + false_easting=10.0, + false_northing=cf.Data(-20, units="m"), + standard_parallel=(None, 50), + longitude_of_projection_origin=-40.0, + scale_factor_at_projection_origin=3.0, + prime_meridian_name="brussels", + ) + self.assertEqual(g1.false_easting, cf.Data(10.0, "m")) + self.assertEqual(g1.false_northing, cf.Data(-20, "m")) + self.assertEqual( + g1.standard_parallel, (None, cf.Data(50, "degrees_north")) + ) + self.assertEqual( + g1.longitude_of_projection_origin, cf.Data(-40.0, "degrees_east") + ) + self.assertEqual(g1.scale_factor_at_projection_origin, cf.Data(3.0, 1)) + self.assertEqual(g1.prime_meridian_name, "brussels") + + # TODOGM extend this test a lot with testing like the above and + # with systematic coverage over valid inputs + @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping__get_cf_grid_mapping_from_name(self): """Test the '_get_cf_grid_mapping_from_name' function.""" From bda010a19975fc246102867040a39eacf72982ec Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 1 Sep 2023 15:56:22 +0100 Subject: [PATCH 64/97] Fix super call defaults to fix failing test --- cf/gridmappings.py | 48 ++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index d7f7cdaed3..8384c09681 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -948,10 +948,10 @@ def __init__( ): super().__init__( perspective_point_height, - longitude_of_projection_origin=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, + longitude_of_projection_origin=longitude_of_projection_origin, + latitude_of_projection_origin=latitude_of_projection_origin, + false_easting=false_easting, + false_northing=false_northing, **kwargs, ) @@ -1170,7 +1170,11 @@ def __init__( longitude_of_central_meridian=0.0, **kwargs, ): - super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) + super().__init__( + false_easting=false_easting, + false_northing=false_northing, + **kwargs, + ) self.standard_parallel = ( _validate_map_parameter("standard_parallel", standard_parallel[0]), @@ -1259,7 +1263,11 @@ def __init__( scale_factor_at_projection_origin=1.0, **kwargs, ): - super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) + super().__init__( + false_easting=false_easting, + false_northing=false_northing, + **kwargs, + ) self.standard_parallel = ( _validate_map_parameter("standard_parallel", standard_parallel[0]), @@ -1348,7 +1356,11 @@ def __init__( false_northing=0.0, **kwargs, ): - super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) + super().__init__( + false_easting=false_easting, + false_northing=false_northing, + **kwargs, + ) self.azimuth_of_central_line = _validate_map_parameter( "azimuth_of_central_line", azimuth_of_central_line @@ -1512,10 +1524,10 @@ def __init__( # TODO check defaults here, they do not appear for # CRS.from_proj4("+proj=ups").to_cf() to cross reference! super().__init__( - latitude_of_projection_origin=0.0, - longitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, + latitude_of_projection_origin=latitude_of_projection_origin, + longitude_of_projection_origin=longitude_of_projection_origin, + false_easting=false_easting, + false_northing=false_northing, **kwargs, ) @@ -1753,10 +1765,10 @@ def __init__( **kwargs, ): super().__init__( - false_easting=0.0, - false_northing=0.0, - longitude_of_projection_origin=0.0, - latitude_of_projection_origin=0.0, + false_easting=false_easting, + false_northing=false_northing, + longitude_of_projection_origin=longitude_of_projection_origin, + latitude_of_projection_origin=latitude_of_projection_origin, **kwargs, ) @@ -1831,7 +1843,11 @@ def __init__( false_northing=0.0, **kwargs, ): - super().__init__(false_easting=0.0, false_northing=0.0, **kwargs) + super().__init__( + false_easting=false_easting, + false_northing=false_northing, + **kwargs, + ) self.scale_factor_at_central_meridian = _validate_map_parameter( "scale_factor_at_central_meridian", From e2e2162ef594c65855b6bc89a2774a641869e227 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 4 Sep 2023 12:57:09 +0100 Subject: [PATCH 65/97] Fix processing of map parameters as Data --- cf/gridmappings.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 8384c09681..e45ebd1b01 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -251,7 +251,7 @@ def _make_proj_string_comp(spec): def _validate_map_parameter(mp_name, mp_value): - """TODO + """Validate map parameters for correct type and canonical units. :Parameters: @@ -263,8 +263,8 @@ def _validate_map_parameter(mp_name, mp_value): :Returns: - `dict` - TODO + `Data`, `str`, or `None` + A value conformed to the TODO """ # 0. Check input parameters are valid CF GM map parameters, not @@ -283,7 +283,7 @@ def _validate_map_parameter(mp_name, mp_value): expect_numeric = cr_gm_valid_attr_names_are_numeric[mp_name] if expect_numeric: if (isinstance(mp_value, Data) and not is_numeric_dtype(mp_value)) or ( - not isinstance(mp_value, (int, float)) + not isinstance(mp_value, (Data, int, float)) ): raise TypeError( f"Map parameter {mp_name} has an incompatible " @@ -316,6 +316,8 @@ def _validate_map_parameter(mp_name, mp_value): conforming_value = Units.conform( mp_value.array.item(), units, canon_units ) + else: + conforming_value = mp_value else: conforming_value = mp_value From bd326fd68c0dcf3d0f74b0b17d384be61671fa92 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 4 Sep 2023 13:42:16 +0100 Subject: [PATCH 66/97] Update map parameter validation test to check unit conformance --- cf/test/test_gridmappings.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 8dfc00fe11..df0aea1b06 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -149,14 +149,16 @@ def test_grid_mapping_map_parameter_validation(self): """Test the validation of map parameters to Grid Mapping classes.""" g1 = cf.Mercator( false_easting=10.0, - false_northing=cf.Data(-20, units="m"), + false_northing=cf.Data(-20, units="cm"), standard_parallel=(None, 50), - longitude_of_projection_origin=-40.0, + longitude_of_projection_origin=cf.Data( + -40.0, units="degrees_east" + ), scale_factor_at_projection_origin=3.0, prime_meridian_name="brussels", ) self.assertEqual(g1.false_easting, cf.Data(10.0, "m")) - self.assertEqual(g1.false_northing, cf.Data(-20, "m")) + self.assertEqual(g1.false_northing, cf.Data(-0.2, "m")) self.assertEqual( g1.standard_parallel, (None, cf.Data(50, "degrees_north")) ) From 2646b6025ac692ff5bfcaaa2e3a2748091cbe011 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 4 Sep 2023 13:47:52 +0100 Subject: [PATCH 67/97] Remove read of own un-hosted netCDF test file for review --- cf/test/test_gridmappings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index df0aea1b06..3321f687da 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -50,10 +50,11 @@ class GridMappingsTest(unittest.TestCase): "f7": cf.RotatedLatitudeLongitude, } + # TODO: ignore the below for now, in short will create a new test file + # with a Oblique Mercator GM + # # From a custom netCDF file with Oblique Mercator GM - # TODO generate this .nc via create_test_files.py and un-commit - # forced commit of the (data-free / header-only) netCDF file. - f_om = cf.read("oblique_mercator.nc") + # f_om = cf.read("oblique_mercator.nc") # Create some coordinate references with different GMs to test on: cr_aea = cf.CoordinateReference( From 75dece63fee2e02d7f8ec8e39946ced388b19da2 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 22 Sep 2023 11:46:28 +0100 Subject: [PATCH 68/97] Update docstring TODOs in gridmappings module --- cf/gridmappings.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index e45ebd1b01..fe31852c63 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -256,15 +256,25 @@ def _validate_map_parameter(mp_name, mp_value): :Parameters: mp_name: `str` - TODO - - mp_value: TODO - TODO + The name of the map parameter to validate. It should be + a name valid in this way in CF, namely one listed under + the 'Table F.1. Grid Mapping Attributes' in Appendix + F: Grid Mappings', therefore listed as a key in + `cr_gm_valid_attr_names_are_numeric`, else a ValueError + will be raised early. + + mp_value: `str`, `Data`, numeric or `None` + The map parameter value being set for the given map + parameter name. The type, and if numeric or `Data`, + units, will be validated against the expected values + for the given map parameter name. :Returns: `Data`, `str`, or `None` - A value conformed to the TODO + The map parameter value, assuming it passes validation, + conformed to the canonical units of the map parameter + name, if units are applicable. """ # 0. Check input parameters are valid CF GM map parameters, not @@ -510,7 +520,16 @@ def get_proj_string(self, params=None): return f"{PROJ_PREFIX}={self.proj_id}{params}" def get_proj_crs(self): - """TODO.""" + """Get the PROJ Coordinate Reference System. + + :Returns: + + `pyproj.crs.CRS` + The PROJ Coordinate Reference System defined with + a `pyproj` `CRS` class that corresponds to the + Grid Mapping instance. + + """ return CRS.from_proj4(self.get_proj_string()) @@ -1924,8 +1943,6 @@ class VerticalPerspective(PerspectiveGridMapping): proj_id = "nsper" -# TODO move this definition elsewhere, having at end feels like an -# anti-pattern... _all_abstract_grid_mappings = ( GridMapping, AzimuthalGridMapping, From bfe3471d7d7128b3ccd22d62c9c79993943cdab5 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 22 Sep 2023 11:54:38 +0100 Subject: [PATCH 69/97] Add has_crs_wkt method to simplify CRS creation --- cf/gridmappings.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cf/gridmappings.py b/cf/gridmappings.py index fe31852c63..3b337e9da8 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -475,6 +475,11 @@ def __init__( "semi_minor_axis", semi_minor_axis ) + # TODO hook this up to the CF CR + self.crs_wkt = None + + # TODO hook up to the CF CR generally: in part 2. + @property @classmethod @abstractmethod @@ -532,6 +537,18 @@ def get_proj_crs(self): """ return CRS.from_proj4(self.get_proj_string()) + def has_crs_wkt(self): + """True if the Grid Mapping has a valid crs_wkt attribute set. + + :Returns: + + `bool` + Whether the Grid Mapping instance has a crs_wkt + attribute set. + + """ + return self.crs_wkt is not None + class AzimuthalGridMapping(GridMapping): """A Grid Mapping with Azimuthal classification. From bb6bf7c6878a5a814cd591a0cdf2fafd317df3b5 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 26 Sep 2023 10:03:38 +0100 Subject: [PATCH 70/97] Move create_2d_lats_and_lons coordinatereference -> fielddomain --- cf/coordinatereference.py | 21 ++------------------- cf/mixin/fielddomain.py | 5 +++++ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/cf/coordinatereference.py b/cf/coordinatereference.py index 3e8f24c021..0834100f52 100644 --- a/cf/coordinatereference.py +++ b/cf/coordinatereference.py @@ -49,25 +49,8 @@ def _totuple(a): def create_2d_lats_and_lons(projection, proj_1d_coors, crs_params): """TODO.""" - # TODO functional code here to go from inputs to lat_data and lon_data - - # Create latitude construct from data - lat_data = cf.Data(numpy.array(0)) # TODO: minimal dummy data for now - lat_dim = cf.DimensionCoordinate(data=lat_data) - lat_dim.set_properties( - {"standard_name": "latitude", - "units": "degrees_north"} - ) - - # Create longitude construct from data - lon_data = cf.Data(numpy.array(0)) # TODO: minimal dummy data for now - lon_dim = cf.DimensionCoordinate(data=lon_data) - lon_dim.set_properties( - {"standard_name": "longitude", - "units": "degrees_east"} - ) - - return lat_dim, lon_dim + # TODO functional code here to go from inputs to lat_data and lon_dat + pass class CoordinateReference(cfdm.CoordinateReference): diff --git a/cf/mixin/fielddomain.py b/cf/mixin/fielddomain.py index 8af4ba4f8c..9166fe6652 100644 --- a/cf/mixin/fielddomain.py +++ b/cf/mixin/fielddomain.py @@ -2611,6 +2611,11 @@ def refs(self, *identities, **filter_kwargs): """Alias for `coordinate_references`.""" return self.coordinate_references(*identities, **filter_kwargs) + def create_2d_lats_and_lons(self): + """TODO.""" + # TODO functional code here to go from input to lat_data and lon_dat + pass + def _create_ancillary_mask_component(mask_shape, ind, compress): """Create an ancillary mask component. From 98f73aed56bbb7ad8f364463bbd9531121e8bbbd Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 26 Sep 2023 14:16:05 +0100 Subject: [PATCH 71/97] Document new API for fielddomain.create_2d_lats_and_lons --- cf/coordinateconversion.py | 2 -- cf/mixin/fielddomain.py | 52 +++++++++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/cf/coordinateconversion.py b/cf/coordinateconversion.py index b3380d31e4..f2a4650138 100644 --- a/cf/coordinateconversion.py +++ b/cf/coordinateconversion.py @@ -1,7 +1,5 @@ import cfdm -from .gridmappings import * # noqa: F403 - class CoordinateConversion(cfdm.CoordinateConversion): """A coordinate conversion component of a coordinate reference diff --git a/cf/mixin/fielddomain.py b/cf/mixin/fielddomain.py index 9166fe6652..7b66db9474 100644 --- a/cf/mixin/fielddomain.py +++ b/cf/mixin/fielddomain.py @@ -2611,10 +2611,56 @@ def refs(self, *identities, **filter_kwargs): """Alias for `coordinate_references`.""" return self.coordinate_references(*identities, **filter_kwargs) - def create_2d_lats_and_lons(self): - """TODO.""" + @_inplace_enabled(default=False) + def create_2d_lats_and_lons( + self, destination_crs="LatLonGridMapping", inplace=False): + """Create 2-dimensional latitude and longitude coordinates. + + The generated coordinates are added to the `{{class}}` as new + auxiliary coordinates if the operation is in-place, else, by + default, a new copy of the `{{class}}` is returned with the + auxiliary coordinates included. + + .. versionadded:: GMVER + + :Parameters: + + destination_crs: `str` + The coordinate reference system that defines the + grid on which to create the latitudes and longitudes. + It should be an identifier which corresponds to the + name of a CF Grid Mapping class. By default, the class + taken is cf.LatLonGridMapping with "LatLonGridMapping". + + .. note:: Creating coordinates on a grid other than + the default cf.LatLonGridMapping is not + yet supported. + + {{inplace: `bool`, optional}} + + :Returns: + + `{{class}}` or `None` + The {{class}} with the newly-created latitude and + longitude coordinates incorporated as new auxiliary + coordinate constructs, or `None` if the operation + was in-place. + + **Examples** + + TODO + + """ + if destination_crs is not "LatLonGridMapping": + raise NotImplementedError( + "Creating latitude and longitude coordinates for " + "a destination grid that is not described by the " + "LatLonGridMapping, the default, is not yet supported." + ) + + f = _inplace_enabled_define_and_cleanup(self) + # TODO functional code here to go from input to lat_data and lon_dat - pass def _create_ancillary_mask_component(mask_shape, ind, compress): From 78a4b6a1a8b64d848df71cf0f8a99ac9f6c30c91 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 26 Sep 2023 14:43:14 +0100 Subject: [PATCH 72/97] Remove functions not needed w/ change of approach --- cf/coordinatereference.py | 6 ------ cf/gridmappings.py | 36 ------------------------------------ 2 files changed, 42 deletions(-) diff --git a/cf/coordinatereference.py b/cf/coordinatereference.py index 0834100f52..9601ba1cbd 100644 --- a/cf/coordinatereference.py +++ b/cf/coordinatereference.py @@ -47,12 +47,6 @@ def _totuple(a): return a -def create_2d_lats_and_lons(projection, proj_1d_coors, crs_params): - """TODO.""" - # TODO functional code here to go from inputs to lat_data and lon_dat - pass - - class CoordinateReference(cfdm.CoordinateReference): """A coordinate reference construct of the CF data model. diff --git a/cf/gridmappings.py b/cf/gridmappings.py index 3b337e9da8..1315539f29 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings.py @@ -1987,39 +1987,3 @@ class VerticalPerspective(PerspectiveGridMapping): TransverseMercator, VerticalPerspective, ) - - -def _get_cf_grid_mapping_from_name(gm_name): - """Return the CF Grid Mapping class for a 'grid_mapping_name'. - - .. versionadded:: GMVER - - :Parameters: - - gm_name: `str` - The value of the 'grid_mapping_name' attribute - to convert to the equivalent CF Grid Mapping class. - - :Returns: - - `cf.GridMapping` - The CF Grid Mapping class corresponding to - the input 'grid_mapping_name' attribute. - - **Examples** - - >>> cf._get_cf_grid_mapping_from_name("vertical_perspective") - cf.gridmappings.VerticalPerspective - - >>> cf._get_cf_grid_mapping_from_name("lambert_conformal_conic") - cf.gridmappings.LambertConformalConic - - - """ - cf_supported_gm_names = { - gm.grid_mapping_name: gm for gm in _all_concrete_grid_mappings - } - if gm_name in cf_supported_gm_names: - return cf_supported_gm_names[gm_name] - else: - return From 05d7b6da26ca7a236a3bf97e1584cff811402fb7 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 26 Sep 2023 15:37:54 +0100 Subject: [PATCH 73/97] Set up new gridmappings module ready to split code up --- cf/__init__.py | 31 ------------- cf/gridmappings/__init__.py | 43 +++++++++++++++++++ .../gridmapping.py} | 8 ++-- setup.py | 1 + 4 files changed, 48 insertions(+), 35 deletions(-) create mode 100644 cf/gridmappings/__init__.py rename cf/{gridmappings.py => gridmappings/gridmapping.py} (99%) diff --git a/cf/__init__.py b/cf/__init__.py index 6a9dc1576b..c5fca32f7a 100644 --- a/cf/__init__.py +++ b/cf/__init__.py @@ -324,37 +324,6 @@ from .regrid import RegridOperator -from .gridmappings import ( - GridMapping, - AzimuthalGridMapping, - ConicGridMapping, - CylindricalGridMapping, - LatLonGridMapping, - PerspectiveGridMapping, - AlbersEqualArea, - AzimuthalEquidistant, - Geostationary, - LambertAzimuthalEqualArea, - LambertConformalConic, - LambertCylindricalEqualArea, - Mercator, - ObliqueMercator, - Orthographic, - PolarStereographic, - RotatedLatitudeLongitude, - LatitudeLongitude, - Sinusoidal, - Stereographic, - TransverseMercator, - VerticalPerspective, - convert_proj_angular_data_to_cf, - convert_cf_angular_data_to_proj, - _all_abstract_grid_mappings, - _all_concrete_grid_mappings, - _get_cf_grid_mapping_from_name, -) - - # Set up basic logging for the full project with a root logger import logging import sys diff --git a/cf/gridmappings/__init__.py b/cf/gridmappings/__init__.py new file mode 100644 index 0000000000..f96d33ff3b --- /dev/null +++ b/cf/gridmappings/__init__.py @@ -0,0 +1,43 @@ +""" +Module for Grid Mappings supported by the CF Conventions. + +For the full list of supported Grid Mappings and details, see Appendix F +of the canonical document: + +https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ +cf-conventions.html#appendix-grid-mappings + +This module should be kept up to date with the Appendix, by adding or +amending appropriate classes. + +""" + +from .gridmapping import ( + GridMapping, + AzimuthalGridMapping, + ConicGridMapping, + CylindricalGridMapping, + LatLonGridMapping, + PerspectiveGridMapping, + AlbersEqualArea, + AzimuthalEquidistant, + Geostationary, + LambertAzimuthalEqualArea, + LambertConformalConic, + LambertCylindricalEqualArea, + Mercator, + ObliqueMercator, + Orthographic, + PolarStereographic, + RotatedLatitudeLongitude, + LatitudeLongitude, + Sinusoidal, + Stereographic, + TransverseMercator, + VerticalPerspective, + convert_proj_angular_data_to_cf, + convert_cf_angular_data_to_proj, + _all_abstract_grid_mappings, + _all_concrete_grid_mappings, + _get_cf_grid_mapping_from_name, +) diff --git a/cf/gridmappings.py b/cf/gridmappings/gridmapping.py similarity index 99% rename from cf/gridmappings.py rename to cf/gridmappings/gridmapping.py index 1315539f29..e55a2e99d1 100644 --- a/cf/gridmappings.py +++ b/cf/gridmappings/gridmapping.py @@ -4,10 +4,10 @@ from pyproj import CRS -from .constants import cr_canonical_units, cr_gm_valid_attr_names_are_numeric -from .data import Data -from .data.utils import is_numeric_dtype -from .units import Units +from ..constants import cr_canonical_units, cr_gm_valid_attr_names_are_numeric +from ..data import Data +from ..data.utils import is_numeric_dtype +from ..units import Units PROJ_PREFIX = "+proj" diff --git a/setup.py b/setup.py index 597f938d40..82d5b5480b 100755 --- a/setup.py +++ b/setup.py @@ -317,6 +317,7 @@ def compile(): "cf.regrid", "cf.umread_lib", "cf.test", + "cf.gridmappings", ], package_data={"cf": package_data}, scripts=["scripts/cfa"], From 63b0f7268831ab1b6bdbdaec6a46f970c04c2620 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 26 Sep 2023 16:09:07 +0100 Subject: [PATCH 74/97] Set up core for gridmappings.abstract sub-sub-module --- cf/gridmappings/__init__.py | 14 +- cf/gridmappings/abstract/abstractall.py | 237 ++++++ cf/gridmappings/abstract/gridmappingbase.py | 532 ++++++++++++++ cf/gridmappings/gridmapping.py | 773 +------------------- setup.py | 1 + 5 files changed, 785 insertions(+), 772 deletions(-) create mode 100644 cf/gridmappings/abstract/abstractall.py create mode 100644 cf/gridmappings/abstract/gridmappingbase.py diff --git a/cf/gridmappings/__init__.py b/cf/gridmappings/__init__.py index f96d33ff3b..ad087c3f89 100644 --- a/cf/gridmappings/__init__.py +++ b/cf/gridmappings/__init__.py @@ -10,15 +10,13 @@ This module should be kept up to date with the Appendix, by adding or amending appropriate classes. +Note that abstract classes to support the creation of concrete classes +for concrete Grid Mappinds are defined in the 'abstract' module, so +not included in the listing below. + """ from .gridmapping import ( - GridMapping, - AzimuthalGridMapping, - ConicGridMapping, - CylindricalGridMapping, - LatLonGridMapping, - PerspectiveGridMapping, AlbersEqualArea, AzimuthalEquidistant, Geostationary, @@ -35,9 +33,5 @@ Stereographic, TransverseMercator, VerticalPerspective, - convert_proj_angular_data_to_cf, - convert_cf_angular_data_to_proj, - _all_abstract_grid_mappings, _all_concrete_grid_mappings, - _get_cf_grid_mapping_from_name, ) diff --git a/cf/gridmappings/abstract/abstractall.py b/cf/gridmappings/abstract/abstractall.py new file mode 100644 index 0000000000..8920bf8d51 --- /dev/null +++ b/cf/gridmappings/abstract/abstractall.py @@ -0,0 +1,237 @@ +import itertools +import re +from abc import ABC, abstractmethod + +from pyproj import CRS + +from ...constants import cr_canonical_units, cr_gm_valid_attr_names_are_numeric +from ...data import Data +from ...data.utils import is_numeric_dtype +from ...units import Units + +from .gridmappingbase import GridMapping + + +class AzimuthalGridMapping(GridMapping): + """A Grid Mapping with Azimuthal classification. + + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + def __init__( + self, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, + false_easting=0.0, + false_northing=0.0, + **kwargs, + ): + super().__init__(**kwargs) + + self.longitude_of_projection_origin = _validate_map_parameter( + "longitude_of_projection_origin", longitude_of_projection_origin + ) + self.latitude_of_projection_origin = _validate_map_parameter( + "latitude_of_projection_origin", latitude_of_projection_origin + ) + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) + + +class ConicGridMapping(GridMapping): + """A Grid Mapping with Conic classification. + + .. versionadded:: GMVER + + :Parameters: + + standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being set for either. + + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. + + The default is (0.0, 0.0), that is 0.0 degrees_north + for the first and second standard parallel values. + + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + def __init__( + self, + standard_parallel, + longitude_of_central_meridian=0.0, + latitude_of_projection_origin=0.0, + false_easting=0.0, + false_northing=0.0, + **kwargs, + ): + super().__init__(**kwargs) + + self.standard_parallel = ( + _validate_map_parameter("standard_parallel", standard_parallel[0]), + _validate_map_parameter("standard_parallel", standard_parallel[1]), + ) + self.longitude_of_central_meridian = _validate_map_parameter( + "longitude_of_central_meridian", longitude_of_central_meridian + ) + self.latitude_of_projection_origin = _validate_map_parameter( + "latitude_of_projection_origin", latitude_of_projection_origin + ) + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) + + +class CylindricalGridMapping(GridMapping): + """A Grid Mapping with Cylindrical classification. + + .. versionadded:: GMVER + + :Parameters: + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + def __init__(self, false_easting=0.0, false_northing=0.0, **kwargs): + super().__init__(**kwargs) + + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) + + +class LatLonGridMapping(GridMapping): + """A Grid Mapping with Latitude-Longitude nature. + + Such a Grid Mapping is based upon latitude and longitude coordinates + on a spherical Earth, defining the canonical 2D geographical coordinate + system so that the figure of the Earth can be described. + + .. versionadded:: GMVER + + """ + + pass + + +class PerspectiveGridMapping(AzimuthalGridMapping): + """A Grid Mapping with Azimuthal classification and perspective view. + + .. versionadded:: GMVER + + :Parameters: + + perspective_point_height: number or scalar `Data` + The height of the view point above the surface (PROJ + 'h') value, for example the height of a satellite above + the Earth, in units of meters 'm'. If provided + as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + + """ + + def __init__(self, perspective_point_height, **kwargs): + super().__init__(**kwargs) + + self.perspective_point_height = _validate_map_parameter( + "perspective_point_height", perspective_point_height + ) + + +_all_abstract_grid_mappings = ( + GridMapping, + AzimuthalGridMapping, + ConicGridMapping, + CylindricalGridMapping, + LatLonGridMapping, + PerspectiveGridMapping, +) diff --git a/cf/gridmappings/abstract/gridmappingbase.py b/cf/gridmappings/abstract/gridmappingbase.py new file mode 100644 index 0000000000..d7f5e2a07d --- /dev/null +++ b/cf/gridmappings/abstract/gridmappingbase.py @@ -0,0 +1,532 @@ +import itertools +import re +from abc import ABC, abstractmethod + +from pyproj import CRS + +from ...constants import cr_canonical_units, cr_gm_valid_attr_names_are_numeric +from ...data import Data +from ...data.utils import is_numeric_dtype +from ...units import Units + + +PROJ_PREFIX = "+proj" + +""" +Define this first since it provides the default for several parameters, +e.g. WGS1984_CF_ATTR_DEFAULTS.semi_major_axis is 6378137.0, the radius +of the Earth in metres. Note we use the 'merc' projection to take these +from since that projection includes all the attributes given for +'latlon' instead and with identical values, but also includes further +map parameters with defaults applied across the projections. + +At the time of dedicating the code, the value of this is as follows, and +the values documented as defaults in the docstrings are taken from this: + +{'crs_wkt': '', + 'semi_major_axis': 6378137.0, + 'semi_minor_axis': 6356752.314245179, + 'inverse_flattening': 298.257223563, + 'reference_ellipsoid_name': 'WGS 84', + 'longitude_of_prime_meridian': 0.0, + 'prime_meridian_name': 'Greenwich', + 'geographic_crs_name': 'unknown', + 'horizontal_datum_name': 'World Geodetic System 1984', + 'projected_crs_name': 'unknown', + 'grid_mapping_name': 'mercator', + 'standard_parallel': 0.0, + 'longitude_of_projection_origin': 0.0, + 'false_easting': 0.0, + 'false_northing': 0.0, + 'scale_factor_at_projection_origin': 1.0} +""" +WGS1984_CF_ATTR_DEFAULTS = CRS.from_proj4("+proj=merc").to_cf() + + +def convert_proj_angular_data_to_cf(proj_data, context=None): + """Convert a PROJ angular data component into CF Data with CF Units. + + PROJ units for latitude and longitude are in + units of decimal degrees, where forming a string by adding + a suffix character indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. + + Note that `cf.convert_cf_angular_data_to_proj` and + this function are not strict inverse functions, + since the former will always convert to the *simplest* + way to specify the PROJ input, namely with no suffix for + degrees(_X) units and the 'R' suffix for radians, whereas + the input might have 'D' or 'r' etc. instead. + + .. versionadded:: GMVER + + :Parameters: + + proj_data: `str` + The PROJ angular data component, for example "90", "90.0", + "90.0D", "1.0R", or "1r". + + For details on valid PROJ data components in PROJ strings, + notably indicating units, see: + + https://proj.org/en/9.2/usage/projections.html#projection-units + + context: `str` or `None`, optional + The physical context of the conversion, where 'lat' indicates + a latitude value and 'lon' indicates a longitude, such that + indication of either context will return cf.Data with values + having units appropriate to that context, namely 'degrees_north' + or 'degrees_east' respectively. If None, 'degrees' or 'radians' + (depending on the input PROJ units) will be the units. + The default is None. + + :Returns: + + `Data` + A cf.Data object with CF-compliant units that corresponds + to the PROJ data and the context provided. + + """ + cf_compatible = True # unless find otherwise (True unless proven False) + if context == "lat": + cf_units = "degrees_north" + elif context == "lon": + cf_units = "degrees_east" + else: + # From the CF Conventions Document (3.1. Units): + # "The COARDS convention prohibits the unit degrees altogether, + # but this unit is not forbidden by the CF convention because + # it may in fact be appropriate for a variable containing, say, + # solar zenith angle. The unit degrees is also allowed on + # coordinate variables such as the latitude and longitude + # coordinates of a transformed grid. In this case the coordinate + # values are not true latitudes and longitudes which must always + # be identified using the more specific forms of degrees as + # described in Section 4.1. + cf_units = "degrees" + + # Only valid input is a valid float or integer (digit with zero or one + # decimal point only) optionally followed by a single suffix letter + # indicating decimal degrees or radians with PROJ. Be strict about an + # exact regex match, because anything not following the pattern (e.g. + # something with extra letters) will be ambiguous for PROJ units. + valid_form = re.compile("(-?\d+(\.\d*)?)([rRdD°]?)") + form = re.fullmatch(valid_form, proj_data) + if form: + comps = form.groups() + suffix = None + if len(comps) == 3: + value, float_comp, suffix = comps + else: + value, *float_comp = comps + + # Convert string value to relevant numeric form + if float_comp: + numeric_value = float(value) + else: + numeric_value = int(value) + + if suffix in ("r", "R"): # radians units + if context: + # Convert so we can apply degree_X form of the lat/lon context + numeric_value = Units.conform( + numeric_value, Units("radians"), Units("degrees") + ) + else: # Otherwise leave as radians to avoid rounding etc. + cf_units = "radians" + elif suffix and suffix not in ("d", "D", "°"): # 'decimal degrees' + cf_compatible = False + else: + cf_compatible = False + + if not cf_compatible: + raise ValueError( + f"PROJ data input not valid: {proj_data}. Ensure a valid " + "PROJ value and optionally units are supplied." + ) + + return Data(numeric_value, Units(cf_units)) + + +def convert_cf_angular_data_to_proj(data): + """Convert singleton angular CF Data into a PROJ data component. + + PROJ units for latitude and longitude are in + units of decimal degrees, where forming a string by adding + a suffix character indicates alternative units of + radians if the suffix is 'R' or 'r'. If a string, a suffix + of 'd', 'D' or '°' confirm units of decimal degrees. + + Note that this function and `convert_proj_angular_data_to_cf` + are not strict inverse functions, since this function + will always convert to the *simplest* + way to specify the PROJ input, namely with no suffix for + degrees(_X) units and the 'R' suffix for radians, whereas + the input might have 'D' or 'r' etc. instead. + + .. versionadded:: GMVER + + :Parameters: + + data: `Data` + A cf.Data object of size 1 containing an angular value + with CF-compliant angular units, for example + cf.Data(45, units="degrees_north"). + + :Returns: + + `str` + A PROJ angular data component that corresponds + to the Data provided. + + """ + if data.size != 1: + raise ValueError( + f"Input cf.Data must have size 1, got size: {data.size}" + ) + if not is_numeric_dtype(data): + raise TypeError( + f"Input cf.Data must have numeric data type, got: {data.dtype}" + ) + + units = data.Units + if not units: + raise ValueError( + "Must provide cf.Data with units for unambiguous conversion." + ) + units_str = units.units + + degrees_unit_prefix = ["degree", "degrees"] + # Taken from 4.1. Latitude Coordinate and 4.2. Longitude Coordinate: + # http://cfconventions.org/cf-conventions/cf-conventions.html... + # ...#latitude-coordinate and ...#longitude-coordinate + valid_cf_lat_lon_units = [ + s + e + for s, e in itertools.product( + degrees_unit_prefix, ("_north", "_N", "N", "_east", "_E", "E") + ) + ] + valid_degrees_units = degrees_unit_prefix + valid_cf_lat_lon_units + + if units_str in valid_degrees_units: + # No need for suffix 'D' for decimal degrees, as that is the default + # recognised when no suffix is given + proj_data = f"{data.data.array.item()}" + elif units_str == "radians": + proj_data = f"{data.data.array.item()}R" + else: + raise ValueError( + "Unrecognised angular units set on the cf.Data. Valid options " + f"are: {', '.join(valid_degrees_units)} and radians but got: " + f"{units_str}" + ) + + return proj_data + + +def _make_proj_string_comp(spec): + """Form a PROJ proj-string end from the given PROJ parameters. + + :Parameters: + + spec: `dict` + A dictionary providing the proj-string specifiers for + parameters, as keys, with their values as values. Values + must be convertible to strings. + + """ + proj_string = "" + for comp, value in spec.items(): + if not isinstance(value, str): + try: + value = str(value) + except TypeError: + raise TypeError( + "Can't create proj-string due to non-representable " + f"value {value} for key {comp}" + ) + proj_string += f" +{comp}={value}" + return proj_string + + +def _validate_map_parameter(mp_name, mp_value): + """Validate map parameters for correct type and canonical units. + + :Parameters: + + mp_name: `str` + The name of the map parameter to validate. It should be + a name valid in this way in CF, namely one listed under + the 'Table F.1. Grid Mapping Attributes' in Appendix + F: Grid Mappings', therefore listed as a key in + `cr_gm_valid_attr_names_are_numeric`, else a ValueError + will be raised early. + + mp_value: `str`, `Data`, numeric or `None` + The map parameter value being set for the given map + parameter name. The type, and if numeric or `Data`, + units, will be validated against the expected values + for the given map parameter name. + + :Returns: + + `Data`, `str`, or `None` + The map parameter value, assuming it passes validation, + conformed to the canonical units of the map parameter + name, if units are applicable. + + """ + # 0. Check input parameters are valid CF GM map parameters, not + # for any case something unrecognised that will silently do nothing. + if mp_name not in cr_gm_valid_attr_names_are_numeric: + raise ValueError( + "Unrecognised map parameter provided for the " + f"Grid Mapping: {mp_name}" + ) + + # 1. If None, can return early: + if mp_value is None: # distinguish from 0 or 0.0 etc. + return None + + # 2. Now ensure the type of the value is as expected. + expect_numeric = cr_gm_valid_attr_names_are_numeric[mp_name] + if expect_numeric: + if (isinstance(mp_value, Data) and not is_numeric_dtype(mp_value)) or ( + not isinstance(mp_value, (Data, int, float)) + ): + raise TypeError( + f"Map parameter {mp_name} has an incompatible " + "data type, expected numeric or Data but got " + f"{type(mp_value)}" + ) + elif not expect_numeric and not isinstance(mp_value, str): + raise TypeError( + f"Map parameter {mp_name} has an incompatible " + "data type, expected a string but got " + f"{type(mp_value)}" + ) + + # 3. Finally ensure the units are valid and conformed to the + # canonical units, where numeric. + if expect_numeric: + canon_units = cr_canonical_units[mp_name] + if isinstance(mp_value, Data): + # In this case, is Data which may have units which aren't equal to + # the canonical ones, so may need to conform the value. + units = mp_value.Units + # The units must be checked and might need to be conformed + if not units.equivalent(canon_units): + raise ValueError( + f"Map parameter {mp_name} value has units that " + "are incompatible with the expected units of " + f"{canon_units}: {units}" + ) + elif not units.equals(canon_units): + conforming_value = Units.conform( + mp_value.array.item(), units, canon_units + ) + else: + conforming_value = mp_value + else: + conforming_value = mp_value + + # Return numeric value as Data with conformed value and canonical units + return Data(conforming_value, units=canon_units) + else: + # Return string value + return mp_value + + +class GridMapping(ABC): + """A container for a Grid Mapping recognised by the CF Conventions.""" + + def __init__( + self, + # i.e. WGS1984_CF_ATTR_DEFAULTS["reference_ellipsoid_name"], etc. + reference_ellipsoid_name="WGS 84", + # The next three parameters are non-zero floats so don't hard-code + # WGS84 defaults in case of future precision changes: + semi_major_axis=WGS1984_CF_ATTR_DEFAULTS["semi_major_axis"], + semi_minor_axis=WGS1984_CF_ATTR_DEFAULTS["semi_minor_axis"], + inverse_flattening=WGS1984_CF_ATTR_DEFAULTS["inverse_flattening"], + prime_meridian_name="Greenwich", + longitude_of_prime_meridian=0.0, + earth_radius=None, + **kwargs, + ): + """**Initialisation** + + :Parameters: + + reference_ellipsoid_name: `str` or `None`, optional + The name of a built-in ellipsoid definition. + The default is "WGS 84". + + .. note:: If used in conjunction with 'earth_radius', + the 'earth_radius' parameter takes precedence. + + inverse_flattening: number or scalar `Data`, optional + The reverse flattening of the ellipsoid (PROJ 'rf' + value), :math:`\frac{1}{f}`, where f corresponds to + the flattening value (PROJ 'f' value) for the + ellipsoid. Unitless, so `Data` must be unitless. + The default is 298.257223563. + + prime_meridian_name: `str`, optional + A predeclared name to define the prime meridian (PROJ + 'pm' value). The default is "Greenwich". Supported + names and corresponding longitudes are listed at: + + https://proj.org/en/9.2/usage/ + projections.html#prime-meridian + + .. note:: If used in conjunction with + 'longitude_of_prime_meridian', this + parameter takes precedence. + + longitude_of_prime_meridian: number or scalar `Data`, optional + The longitude relative to Greenwich of the + prime meridian. If provided as a number or `Data` without + units, the units are taken as 'degrees_east', else the + `Data` units are taken and must be angular and + compatible with longitude. The default is 0.0 + degrees_east. + + .. note:: If used in conjunction with + 'prime_meridian_name', the + 'prime_meridian_name' parameter takes + precedence. + + semi_major_axis: number, scalar `Data` or `None`, optional + The semi-major axis of the ellipsoid (PROJ 'a' value), + in units of meters unless units are otherwise specified + via `Data` units, in which case they must be conformable + to meters and will be converted. The default is 6378137.0. + + semi_minor_axis: number, scalar `Data` or `None`, optional + The semi-minor axis of the ellipsoid (PROJ 'b' value) + in units of meters unless units are otherwise specified + via `Data` units, in which case they must be conformable + to meters and will be converted. The default is + 6356752.314245179. + + earth_radius: number, scalar `Data` or `None`, optional + The radius of the ellipsoid, if a sphere (PROJ 'R' value), + in units of meters unless units are otherwise specified + via `Data` units, in which case they must be conformable + to meters and will be converted. + + If the ellipsoid is not a sphere, then + set as `None`, the default, to indicate that ellipsoid + parameters such as the reference_ellipsoid_name or + semi_major_axis and semi_minor_axis are being set, + since these will should be used to define a + non-spherical ellipsoid. + + .. note:: If used in conjunction with + 'reference_ellipsoid_name', this parameter + takes precedence. + + """ + # Validate the arbitary kwargs + for kwarg, value in kwargs.items(): + _validate_map_parameter(kwarg, value) + + # The attributes which describe the ellipsoid and prime meridian, + # which may be included, when applicable, with any grid mapping. + self.earth_radius = _validate_map_parameter( + "earth_radius", earth_radius + ) + self.inverse_flattening = _validate_map_parameter( + "inverse_flattening", inverse_flattening + ) + self.longitude_of_prime_meridian = _validate_map_parameter( + "longitude_of_prime_meridian", longitude_of_prime_meridian + ) + self.prime_meridian_name = _validate_map_parameter( + "prime_meridian_name", prime_meridian_name + ) + self.reference_ellipsoid_name = _validate_map_parameter( + "reference_ellipsoid_name", reference_ellipsoid_name + ) + self.semi_major_axis = _validate_map_parameter( + "semi_major_axis", semi_major_axis + ) + self.semi_minor_axis = _validate_map_parameter( + "semi_minor_axis", semi_minor_axis + ) + + # TODO hook this up to the CF CR + self.crs_wkt = None + + # TODO hook up to the CF CR generally: in part 2. + + @property + @classmethod + @abstractmethod + def grid_mapping_name(cls): + """The value of the 'grid_mapping_name' attribute.""" + return + + @property + @classmethod + @abstractmethod + def proj_id(cls): + """The PROJ projection identifier shorthand name.""" + return + + def __repr__(self): + """x.__repr__() <==> repr(x)""" + # Report parent GridMapping class to indicate classification, + # but only if it has one (> 2 avoids own class and 'object') + # base. E.g. we get , + # , . + parent_gm = "" + if len(self.__class__.__mro__) > 2: + parent_gm = self.__class__.__mro__[1].__name__ + ": " + return f"" + + def __str__(self): + """x.__str__() <==> str(x)""" + return f"{self.__repr__()[:-1]} {self.get_proj_string()}>" + + def __eq__(self, other): + """The rich comparison operator ``==``.""" + return self.get_proj_crs() == other.get_proj_crs() + + def __hash__(self, other): + """The built-in function `hash`, x.__hash__() <==> hash(x).""" + return hash(self.get_proj_crs()) + + def get_proj_string(self, params=None): + """The value of the PROJ proj-string defining the projection.""" + # TODO enable parameter input and sync'ing + if not params: + params = "" + return f"{PROJ_PREFIX}={self.proj_id}{params}" + + def get_proj_crs(self): + """Get the PROJ Coordinate Reference System. + + :Returns: + + `pyproj.crs.CRS` + The PROJ Coordinate Reference System defined with + a `pyproj` `CRS` class that corresponds to the + Grid Mapping instance. + + """ + return CRS.from_proj4(self.get_proj_string()) + + def has_crs_wkt(self): + """True if the Grid Mapping has a valid crs_wkt attribute set. + + :Returns: + + `bool` + Whether the Grid Mapping instance has a crs_wkt + attribute set. + + """ + return self.crs_wkt is not None diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index e55a2e99d1..cce273154a 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -9,760 +9,17 @@ from ..data.utils import is_numeric_dtype from ..units import Units -PROJ_PREFIX = "+proj" - - -""" -Define this first since it provides the default for several parameters, -e.g. WGS1984_CF_ATTR_DEFAULTS.semi_major_axis is 6378137.0, the radius -of the Earth in metres. Note we use the 'merc' projection to take these -from since that projection includes all the attributes given for -'latlon' instead and with identical values, but also includes further -map parameters with defaults applied across the projections. - -At the time of dedicating the code, the value of this is as follows, and -the values documented as defaults in the docstrings are taken from this: - -{'crs_wkt': '', - 'semi_major_axis': 6378137.0, - 'semi_minor_axis': 6356752.314245179, - 'inverse_flattening': 298.257223563, - 'reference_ellipsoid_name': 'WGS 84', - 'longitude_of_prime_meridian': 0.0, - 'prime_meridian_name': 'Greenwich', - 'geographic_crs_name': 'unknown', - 'horizontal_datum_name': 'World Geodetic System 1984', - 'projected_crs_name': 'unknown', - 'grid_mapping_name': 'mercator', - 'standard_parallel': 0.0, - 'longitude_of_projection_origin': 0.0, - 'false_easting': 0.0, - 'false_northing': 0.0, - 'scale_factor_at_projection_origin': 1.0} -""" -WGS1984_CF_ATTR_DEFAULTS = CRS.from_proj4("+proj=merc").to_cf() - - -def convert_proj_angular_data_to_cf(proj_data, context=None): - """Convert a PROJ angular data component into CF Data with CF Units. - - PROJ units for latitude and longitude are in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. - - Note that `cf.convert_cf_angular_data_to_proj` and - this function are not strict inverse functions, - since the former will always convert to the *simplest* - way to specify the PROJ input, namely with no suffix for - degrees(_X) units and the 'R' suffix for radians, whereas - the input might have 'D' or 'r' etc. instead. - - .. versionadded:: GMVER - - :Parameters: - - proj_data: `str` - The PROJ angular data component, for example "90", "90.0", - "90.0D", "1.0R", or "1r". - - For details on valid PROJ data components in PROJ strings, - notably indicating units, see: - - https://proj.org/en/9.2/usage/projections.html#projection-units - - context: `str` or `None`, optional - The physical context of the conversion, where 'lat' indicates - a latitude value and 'lon' indicates a longitude, such that - indication of either context will return cf.Data with values - having units appropriate to that context, namely 'degrees_north' - or 'degrees_east' respectively. If None, 'degrees' or 'radians' - (depending on the input PROJ units) will be the units. - The default is None. - - :Returns: - - `Data` - A cf.Data object with CF-compliant units that corresponds - to the PROJ data and the context provided. - - """ - cf_compatible = True # unless find otherwise (True unless proven False) - if context == "lat": - cf_units = "degrees_north" - elif context == "lon": - cf_units = "degrees_east" - else: - # From the CF Conventions Document (3.1. Units): - # "The COARDS convention prohibits the unit degrees altogether, - # but this unit is not forbidden by the CF convention because - # it may in fact be appropriate for a variable containing, say, - # solar zenith angle. The unit degrees is also allowed on - # coordinate variables such as the latitude and longitude - # coordinates of a transformed grid. In this case the coordinate - # values are not true latitudes and longitudes which must always - # be identified using the more specific forms of degrees as - # described in Section 4.1. - cf_units = "degrees" - - # Only valid input is a valid float or integer (digit with zero or one - # decimal point only) optionally followed by a single suffix letter - # indicating decimal degrees or radians with PROJ. Be strict about an - # exact regex match, because anything not following the pattern (e.g. - # something with extra letters) will be ambiguous for PROJ units. - valid_form = re.compile("(-?\d+(\.\d*)?)([rRdD°]?)") - form = re.fullmatch(valid_form, proj_data) - if form: - comps = form.groups() - suffix = None - if len(comps) == 3: - value, float_comp, suffix = comps - else: - value, *float_comp = comps - - # Convert string value to relevant numeric form - if float_comp: - numeric_value = float(value) - else: - numeric_value = int(value) - - if suffix in ("r", "R"): # radians units - if context: - # Convert so we can apply degree_X form of the lat/lon context - numeric_value = Units.conform( - numeric_value, Units("radians"), Units("degrees") - ) - else: # Otherwise leave as radians to avoid rounding etc. - cf_units = "radians" - elif suffix and suffix not in ("d", "D", "°"): # 'decimal degrees' - cf_compatible = False - else: - cf_compatible = False - - if not cf_compatible: - raise ValueError( - f"PROJ data input not valid: {proj_data}. Ensure a valid " - "PROJ value and optionally units are supplied." - ) - - return Data(numeric_value, Units(cf_units)) - - -def convert_cf_angular_data_to_proj(data): - """Convert singleton angular CF Data into a PROJ data component. - - PROJ units for latitude and longitude are in - units of decimal degrees, where forming a string by adding - a suffix character indicates alternative units of - radians if the suffix is 'R' or 'r'. If a string, a suffix - of 'd', 'D' or '°' confirm units of decimal degrees. - - Note that this function and `convert_proj_angular_data_to_cf` - are not strict inverse functions, since this function - will always convert to the *simplest* - way to specify the PROJ input, namely with no suffix for - degrees(_X) units and the 'R' suffix for radians, whereas - the input might have 'D' or 'r' etc. instead. - - .. versionadded:: GMVER - - :Parameters: - - data: `Data` - A cf.Data object of size 1 containing an angular value - with CF-compliant angular units, for example - cf.Data(45, units="degrees_north"). - - :Returns: - - `str` - A PROJ angular data component that corresponds - to the Data provided. - - """ - if data.size != 1: - raise ValueError( - f"Input cf.Data must have size 1, got size: {data.size}" - ) - if not is_numeric_dtype(data): - raise TypeError( - f"Input cf.Data must have numeric data type, got: {data.dtype}" - ) - - units = data.Units - if not units: - raise ValueError( - "Must provide cf.Data with units for unambiguous conversion." - ) - units_str = units.units - - degrees_unit_prefix = ["degree", "degrees"] - # Taken from 4.1. Latitude Coordinate and 4.2. Longitude Coordinate: - # http://cfconventions.org/cf-conventions/cf-conventions.html... - # ...#latitude-coordinate and ...#longitude-coordinate - valid_cf_lat_lon_units = [ - s + e - for s, e in itertools.product( - degrees_unit_prefix, ("_north", "_N", "N", "_east", "_E", "E") - ) - ] - valid_degrees_units = degrees_unit_prefix + valid_cf_lat_lon_units - - if units_str in valid_degrees_units: - # No need for suffix 'D' for decimal degrees, as that is the default - # recognised when no suffix is given - proj_data = f"{data.data.array.item()}" - elif units_str == "radians": - proj_data = f"{data.data.array.item()}R" - else: - raise ValueError( - "Unrecognised angular units set on the cf.Data. Valid options " - f"are: {', '.join(valid_degrees_units)} and radians but got: " - f"{units_str}" - ) - - return proj_data - - -def _make_proj_string_comp(spec): - """Form a PROJ proj-string end from the given PROJ parameters. - - :Parameters: - - spec: `dict` - A dictionary providing the proj-string specifiers for - parameters, as keys, with their values as values. Values - must be convertible to strings. - - """ - proj_string = "" - for comp, value in spec.items(): - if not isinstance(value, str): - try: - value = str(value) - except TypeError: - raise TypeError( - "Can't create proj-string due to non-representable " - f"value {value} for key {comp}" - ) - proj_string += f" +{comp}={value}" - return proj_string - - -def _validate_map_parameter(mp_name, mp_value): - """Validate map parameters for correct type and canonical units. - - :Parameters: - - mp_name: `str` - The name of the map parameter to validate. It should be - a name valid in this way in CF, namely one listed under - the 'Table F.1. Grid Mapping Attributes' in Appendix - F: Grid Mappings', therefore listed as a key in - `cr_gm_valid_attr_names_are_numeric`, else a ValueError - will be raised early. - - mp_value: `str`, `Data`, numeric or `None` - The map parameter value being set for the given map - parameter name. The type, and if numeric or `Data`, - units, will be validated against the expected values - for the given map parameter name. - - :Returns: - - `Data`, `str`, or `None` - The map parameter value, assuming it passes validation, - conformed to the canonical units of the map parameter - name, if units are applicable. - - """ - # 0. Check input parameters are valid CF GM map parameters, not - # for any case something unrecognised that will silently do nothing. - if mp_name not in cr_gm_valid_attr_names_are_numeric: - raise ValueError( - "Unrecognised map parameter provided for the " - f"Grid Mapping: {mp_name}" - ) - - # 1. If None, can return early: - if mp_value is None: # distinguish from 0 or 0.0 etc. - return None - - # 2. Now ensure the type of the value is as expected. - expect_numeric = cr_gm_valid_attr_names_are_numeric[mp_name] - if expect_numeric: - if (isinstance(mp_value, Data) and not is_numeric_dtype(mp_value)) or ( - not isinstance(mp_value, (Data, int, float)) - ): - raise TypeError( - f"Map parameter {mp_name} has an incompatible " - "data type, expected numeric or Data but got " - f"{type(mp_value)}" - ) - elif not expect_numeric and not isinstance(mp_value, str): - raise TypeError( - f"Map parameter {mp_name} has an incompatible " - "data type, expected a string but got " - f"{type(mp_value)}" - ) - - # 3. Finally ensure the units are valid and conformed to the - # canonical units, where numeric. - if expect_numeric: - canon_units = cr_canonical_units[mp_name] - if isinstance(mp_value, Data): - # In this case, is Data which may have units which aren't equal to - # the canonical ones, so may need to conform the value. - units = mp_value.Units - # The units must be checked and might need to be conformed - if not units.equivalent(canon_units): - raise ValueError( - f"Map parameter {mp_name} value has units that " - "are incompatible with the expected units of " - f"{canon_units}: {units}" - ) - elif not units.equals(canon_units): - conforming_value = Units.conform( - mp_value.array.item(), units, canon_units - ) - else: - conforming_value = mp_value - else: - conforming_value = mp_value - - # Return numeric value as Data with conformed value and canonical units - return Data(conforming_value, units=canon_units) - else: - # Return string value - return mp_value - - -"""Abstract classes for general Grid Mappings. - -Note that default arguments are based upon the PROJ defaults, which can -be cross-referenced via running: - -CRS.from_proj4("+proj= ").to_cf() - -where is for when required arguments must be provided -to return a coordinate reference instance, and obviously these values -where reported should not be included as defaults. An example is: - -CRS.from_proj4("+proj=lcc +lat_1=1").to_cf() - -where `'standard_parallel': (1.0, 0.0)` would not be taken as a default. - -""" - - -class GridMapping(ABC): - """A container for a Grid Mapping recognised by the CF Conventions.""" - - def __init__( - self, - # i.e. WGS1984_CF_ATTR_DEFAULTS["reference_ellipsoid_name"], etc. - reference_ellipsoid_name="WGS 84", - # The next three parameters are non-zero floats so don't hard-code - # WGS84 defaults in case of future precision changes: - semi_major_axis=WGS1984_CF_ATTR_DEFAULTS["semi_major_axis"], - semi_minor_axis=WGS1984_CF_ATTR_DEFAULTS["semi_minor_axis"], - inverse_flattening=WGS1984_CF_ATTR_DEFAULTS["inverse_flattening"], - prime_meridian_name="Greenwich", - longitude_of_prime_meridian=0.0, - earth_radius=None, - **kwargs, - ): - """**Initialisation** - - :Parameters: - - reference_ellipsoid_name: `str` or `None`, optional - The name of a built-in ellipsoid definition. - The default is "WGS 84". - - .. note:: If used in conjunction with 'earth_radius', - the 'earth_radius' parameter takes precedence. - - inverse_flattening: number or scalar `Data`, optional - The reverse flattening of the ellipsoid (PROJ 'rf' - value), :math:`\frac{1}{f}`, where f corresponds to - the flattening value (PROJ 'f' value) for the - ellipsoid. Unitless, so `Data` must be unitless. - The default is 298.257223563. - - prime_meridian_name: `str`, optional - A predeclared name to define the prime meridian (PROJ - 'pm' value). The default is "Greenwich". Supported - names and corresponding longitudes are listed at: - - https://proj.org/en/9.2/usage/ - projections.html#prime-meridian - - .. note:: If used in conjunction with - 'longitude_of_prime_meridian', this - parameter takes precedence. - - longitude_of_prime_meridian: number or scalar `Data`, optional - The longitude relative to Greenwich of the - prime meridian. If provided as a number or `Data` without - units, the units are taken as 'degrees_east', else the - `Data` units are taken and must be angular and - compatible with longitude. The default is 0.0 - degrees_east. - - .. note:: If used in conjunction with - 'prime_meridian_name', the - 'prime_meridian_name' parameter takes - precedence. - - semi_major_axis: number, scalar `Data` or `None`, optional - The semi-major axis of the ellipsoid (PROJ 'a' value), - in units of meters unless units are otherwise specified - via `Data` units, in which case they must be conformable - to meters and will be converted. The default is 6378137.0. - - semi_minor_axis: number, scalar `Data` or `None`, optional - The semi-minor axis of the ellipsoid (PROJ 'b' value) - in units of meters unless units are otherwise specified - via `Data` units, in which case they must be conformable - to meters and will be converted. The default is - 6356752.314245179. - - earth_radius: number, scalar `Data` or `None`, optional - The radius of the ellipsoid, if a sphere (PROJ 'R' value), - in units of meters unless units are otherwise specified - via `Data` units, in which case they must be conformable - to meters and will be converted. - - If the ellipsoid is not a sphere, then - set as `None`, the default, to indicate that ellipsoid - parameters such as the reference_ellipsoid_name or - semi_major_axis and semi_minor_axis are being set, - since these will should be used to define a - non-spherical ellipsoid. - - .. note:: If used in conjunction with - 'reference_ellipsoid_name', this parameter - takes precedence. - - """ - # Validate the arbitary kwargs - for kwarg, value in kwargs.items(): - _validate_map_parameter(kwarg, value) - - # The attributes which describe the ellipsoid and prime meridian, - # which may be included, when applicable, with any grid mapping. - self.earth_radius = _validate_map_parameter( - "earth_radius", earth_radius - ) - self.inverse_flattening = _validate_map_parameter( - "inverse_flattening", inverse_flattening - ) - self.longitude_of_prime_meridian = _validate_map_parameter( - "longitude_of_prime_meridian", longitude_of_prime_meridian - ) - self.prime_meridian_name = _validate_map_parameter( - "prime_meridian_name", prime_meridian_name - ) - self.reference_ellipsoid_name = _validate_map_parameter( - "reference_ellipsoid_name", reference_ellipsoid_name - ) - self.semi_major_axis = _validate_map_parameter( - "semi_major_axis", semi_major_axis - ) - self.semi_minor_axis = _validate_map_parameter( - "semi_minor_axis", semi_minor_axis - ) - - # TODO hook this up to the CF CR - self.crs_wkt = None - - # TODO hook up to the CF CR generally: in part 2. - - @property - @classmethod - @abstractmethod - def grid_mapping_name(cls): - """The value of the 'grid_mapping_name' attribute.""" - return - - @property - @classmethod - @abstractmethod - def proj_id(cls): - """The PROJ projection identifier shorthand name.""" - return - - def __repr__(self): - """x.__repr__() <==> repr(x)""" - # Report parent GridMapping class to indicate classification, - # but only if it has one (> 2 avoids own class and 'object') - # base. E.g. we get , - # , . - parent_gm = "" - if len(self.__class__.__mro__) > 2: - parent_gm = self.__class__.__mro__[1].__name__ + ": " - return f"" - - def __str__(self): - """x.__str__() <==> str(x)""" - return f"{self.__repr__()[:-1]} {self.get_proj_string()}>" - - def __eq__(self, other): - """The rich comparison operator ``==``.""" - return self.get_proj_crs() == other.get_proj_crs() - - def __hash__(self, other): - """The built-in function `hash`, x.__hash__() <==> hash(x).""" - return hash(self.get_proj_crs()) - - def get_proj_string(self, params=None): - """The value of the PROJ proj-string defining the projection.""" - # TODO enable parameter input and sync'ing - if not params: - params = "" - return f"{PROJ_PREFIX}={self.proj_id}{params}" - - def get_proj_crs(self): - """Get the PROJ Coordinate Reference System. - - :Returns: - - `pyproj.crs.CRS` - The PROJ Coordinate Reference System defined with - a `pyproj` `CRS` class that corresponds to the - Grid Mapping instance. - - """ - return CRS.from_proj4(self.get_proj_string()) - - def has_crs_wkt(self): - """True if the Grid Mapping has a valid crs_wkt attribute set. - - :Returns: - - `bool` - Whether the Grid Mapping instance has a crs_wkt - attribute set. - - """ - return self.crs_wkt is not None - - -class AzimuthalGridMapping(GridMapping): - """A Grid Mapping with Azimuthal classification. - - .. versionadded:: GMVER - - :Parameters: - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - def __init__( - self, - longitude_of_projection_origin=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - **kwargs, - ): - super().__init__(**kwargs) - - self.longitude_of_projection_origin = _validate_map_parameter( - "longitude_of_projection_origin", longitude_of_projection_origin - ) - self.latitude_of_projection_origin = _validate_map_parameter( - "latitude_of_projection_origin", latitude_of_projection_origin - ) - self.false_easting = _validate_map_parameter( - "false_easting", false_easting - ) - self.false_northing = _validate_map_parameter( - "false_northing", false_northing - ) - - -class ConicGridMapping(GridMapping): - """A Grid Mapping with Conic classification. - - .. versionadded:: GMVER - - :Parameters: - - standard_parallel: 2-`tuple` of number or scalar `Data` or `None` - The standard parallel value(s): the first (PROJ 'lat_1' - value) and/or the second (PROJ 'lat_2' value), given - as a 2-tuple of numbers or strings corresponding to - the first and then the second in order, where `None` - indicates that a value is not being set for either. - - If provided as a number or `Data` without units, the units - for each of the values are taken as 'degrees_north', else - the `Data` units are taken and must be angular and - compatible with latitude. - - The default is (0.0, 0.0), that is 0.0 degrees_north - for the first and second standard parallel values. - - longitude_of_central_meridian: number or scalar `Data`, optional - The longitude of (natural) origin i.e. central meridian. - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - def __init__( - self, - standard_parallel, - longitude_of_central_meridian=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - **kwargs, - ): - super().__init__(**kwargs) - - self.standard_parallel = ( - _validate_map_parameter("standard_parallel", standard_parallel[0]), - _validate_map_parameter("standard_parallel", standard_parallel[1]), - ) - self.longitude_of_central_meridian = _validate_map_parameter( - "longitude_of_central_meridian", longitude_of_central_meridian - ) - self.latitude_of_projection_origin = _validate_map_parameter( - "latitude_of_projection_origin", latitude_of_projection_origin - ) - self.false_easting = _validate_map_parameter( - "false_easting", false_easting - ) - self.false_northing = _validate_map_parameter( - "false_northing", false_northing - ) - - -class CylindricalGridMapping(GridMapping): - """A Grid Mapping with Cylindrical classification. - - .. versionadded:: GMVER - - :Parameters: - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - def __init__(self, false_easting=0.0, false_northing=0.0, **kwargs): - super().__init__(**kwargs) - - self.false_easting = _validate_map_parameter( - "false_easting", false_easting - ) - self.false_northing = _validate_map_parameter( - "false_northing", false_northing - ) - - -class LatLonGridMapping(GridMapping): - """A Grid Mapping with Latitude-Longitude nature. - - Such a Grid Mapping is based upon latitude and longitude coordinates - on a spherical Earth, defining the canonical 2D geographical coordinate - system so that the figure of the Earth can be described. - - .. versionadded:: GMVER - - """ - - pass - - -class PerspectiveGridMapping(AzimuthalGridMapping): - """A Grid Mapping with Azimuthal classification and perspective view. - - .. versionadded:: GMVER - - :Parameters: - - perspective_point_height: number or scalar `Data` - The height of the view point above the surface (PROJ - 'h') value, for example the height of a satellite above - the Earth, in units of meters 'm'. If provided - as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - - """ - - def __init__(self, perspective_point_height, **kwargs): - super().__init__(**kwargs) - - self.perspective_point_height = _validate_map_parameter( - "perspective_point_height", perspective_point_height - ) +from .abstract import ( + GridMapping, + AzimuthalGridMapping, + ConicGridMapping, + CylindricalGridMapping, + LatLonGridMapping, + PerspectiveGridMapping, + _all_abstract_grid_mappings, + convert_proj_angular_data_to_cf, + convert_cf_angular_data_to_proj, +) """Concrete classes for all Grid Mappings supported by the CF Conventions. @@ -1960,14 +1217,6 @@ class VerticalPerspective(PerspectiveGridMapping): proj_id = "nsper" -_all_abstract_grid_mappings = ( - GridMapping, - AzimuthalGridMapping, - ConicGridMapping, - CylindricalGridMapping, - LatLonGridMapping, - PerspectiveGridMapping, -) # Representing all Grid Mappings repsented by the CF Conventions (Appendix F) _all_concrete_grid_mappings = ( AlbersEqualArea, diff --git a/setup.py b/setup.py index 82d5b5480b..ca0401725f 100755 --- a/setup.py +++ b/setup.py @@ -318,6 +318,7 @@ def compile(): "cf.umread_lib", "cf.test", "cf.gridmappings", + "cf.gridmappings.abstract", ], package_data={"cf": package_data}, scripts=["scripts/cfa"], From e446b78d6af0f54474bc5b6fdf71af49b6ce380e Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 26 Sep 2023 17:58:20 +0100 Subject: [PATCH 75/97] Separate abstract GM classes out into individual modules --- cf/gridmappings/abstract/__init__.py | 25 +++ cf/gridmappings/abstract/abstractall.py | 237 ------------------------ cf/gridmappings/abstract/azimuthal.py | 65 +++++++ cf/gridmappings/abstract/conic.py | 85 +++++++++ cf/gridmappings/abstract/cylindrical.py | 38 ++++ cf/gridmappings/abstract/latlon.py | 18 ++ cf/gridmappings/abstract/perspective.py | 27 +++ cf/gridmappings/gridmapping.py | 3 - cf/test/test_gridmappings.py | 10 + 9 files changed, 268 insertions(+), 240 deletions(-) create mode 100644 cf/gridmappings/abstract/__init__.py delete mode 100644 cf/gridmappings/abstract/abstractall.py create mode 100644 cf/gridmappings/abstract/azimuthal.py create mode 100644 cf/gridmappings/abstract/conic.py create mode 100644 cf/gridmappings/abstract/cylindrical.py create mode 100644 cf/gridmappings/abstract/latlon.py create mode 100644 cf/gridmappings/abstract/perspective.py diff --git a/cf/gridmappings/abstract/__init__.py b/cf/gridmappings/abstract/__init__.py new file mode 100644 index 0000000000..0712560fe5 --- /dev/null +++ b/cf/gridmappings/abstract/__init__.py @@ -0,0 +1,25 @@ +""" +Abstract classes representing categories of Grid Mapping. + +Categories correspond to the type of projection a Grid Mapping is +based upon, for example these are often based upon a geometric shape +that forms the developable surface that is used to flatten the map +of Earth (such as conic for a cone or cylindrical for a cylinder). + +The LatLonGridMapping case is special in that it (from the CF Conventions, +Appendix F) 'defines the canonical 2D geographical coordinate system +based upon latitude and longitude coordinates on a spherical Earth'. + +""" + +from .gridmappingbase import ( + GridMapping, + convert_proj_angular_data_to_cf, + convert_cf_angular_data_to_proj, + _validate_map_parameter, +) +from .azimuthal import AzimuthalGridMapping +from .conic import ConicGridMapping +from .cylindrical import CylindricalGridMapping +from .latlon import LatLonGridMapping +from .perspective import PerspectiveGridMapping diff --git a/cf/gridmappings/abstract/abstractall.py b/cf/gridmappings/abstract/abstractall.py deleted file mode 100644 index 8920bf8d51..0000000000 --- a/cf/gridmappings/abstract/abstractall.py +++ /dev/null @@ -1,237 +0,0 @@ -import itertools -import re -from abc import ABC, abstractmethod - -from pyproj import CRS - -from ...constants import cr_canonical_units, cr_gm_valid_attr_names_are_numeric -from ...data import Data -from ...data.utils import is_numeric_dtype -from ...units import Units - -from .gridmappingbase import GridMapping - - -class AzimuthalGridMapping(GridMapping): - """A Grid Mapping with Azimuthal classification. - - .. versionadded:: GMVER - - :Parameters: - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - def __init__( - self, - longitude_of_projection_origin=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - **kwargs, - ): - super().__init__(**kwargs) - - self.longitude_of_projection_origin = _validate_map_parameter( - "longitude_of_projection_origin", longitude_of_projection_origin - ) - self.latitude_of_projection_origin = _validate_map_parameter( - "latitude_of_projection_origin", latitude_of_projection_origin - ) - self.false_easting = _validate_map_parameter( - "false_easting", false_easting - ) - self.false_northing = _validate_map_parameter( - "false_northing", false_northing - ) - - -class ConicGridMapping(GridMapping): - """A Grid Mapping with Conic classification. - - .. versionadded:: GMVER - - :Parameters: - - standard_parallel: 2-`tuple` of number or scalar `Data` or `None` - The standard parallel value(s): the first (PROJ 'lat_1' - value) and/or the second (PROJ 'lat_2' value), given - as a 2-tuple of numbers or strings corresponding to - the first and then the second in order, where `None` - indicates that a value is not being set for either. - - If provided as a number or `Data` without units, the units - for each of the values are taken as 'degrees_north', else - the `Data` units are taken and must be angular and - compatible with latitude. - - The default is (0.0, 0.0), that is 0.0 degrees_north - for the first and second standard parallel values. - - longitude_of_central_meridian: number or scalar `Data`, optional - The longitude of (natural) origin i.e. central meridian. - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - def __init__( - self, - standard_parallel, - longitude_of_central_meridian=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - **kwargs, - ): - super().__init__(**kwargs) - - self.standard_parallel = ( - _validate_map_parameter("standard_parallel", standard_parallel[0]), - _validate_map_parameter("standard_parallel", standard_parallel[1]), - ) - self.longitude_of_central_meridian = _validate_map_parameter( - "longitude_of_central_meridian", longitude_of_central_meridian - ) - self.latitude_of_projection_origin = _validate_map_parameter( - "latitude_of_projection_origin", latitude_of_projection_origin - ) - self.false_easting = _validate_map_parameter( - "false_easting", false_easting - ) - self.false_northing = _validate_map_parameter( - "false_northing", false_northing - ) - - -class CylindricalGridMapping(GridMapping): - """A Grid Mapping with Cylindrical classification. - - .. versionadded:: GMVER - - :Parameters: - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - def __init__(self, false_easting=0.0, false_northing=0.0, **kwargs): - super().__init__(**kwargs) - - self.false_easting = _validate_map_parameter( - "false_easting", false_easting - ) - self.false_northing = _validate_map_parameter( - "false_northing", false_northing - ) - - -class LatLonGridMapping(GridMapping): - """A Grid Mapping with Latitude-Longitude nature. - - Such a Grid Mapping is based upon latitude and longitude coordinates - on a spherical Earth, defining the canonical 2D geographical coordinate - system so that the figure of the Earth can be described. - - .. versionadded:: GMVER - - """ - - pass - - -class PerspectiveGridMapping(AzimuthalGridMapping): - """A Grid Mapping with Azimuthal classification and perspective view. - - .. versionadded:: GMVER - - :Parameters: - - perspective_point_height: number or scalar `Data` - The height of the view point above the surface (PROJ - 'h') value, for example the height of a satellite above - the Earth, in units of meters 'm'. If provided - as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - - """ - - def __init__(self, perspective_point_height, **kwargs): - super().__init__(**kwargs) - - self.perspective_point_height = _validate_map_parameter( - "perspective_point_height", perspective_point_height - ) - - -_all_abstract_grid_mappings = ( - GridMapping, - AzimuthalGridMapping, - ConicGridMapping, - CylindricalGridMapping, - LatLonGridMapping, - PerspectiveGridMapping, -) diff --git a/cf/gridmappings/abstract/azimuthal.py b/cf/gridmappings/abstract/azimuthal.py new file mode 100644 index 0000000000..e6f646d4c3 --- /dev/null +++ b/cf/gridmappings/abstract/azimuthal.py @@ -0,0 +1,65 @@ +from .gridmappingbase import ( + GridMapping, + _validate_map_parameter, +) + + +class AzimuthalGridMapping(GridMapping): + """A Grid Mapping with Azimuthal classification. + + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + def __init__( + self, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, + false_easting=0.0, + false_northing=0.0, + **kwargs, + ): + super().__init__(**kwargs) + + self.longitude_of_projection_origin = _validate_map_parameter( + "longitude_of_projection_origin", longitude_of_projection_origin + ) + self.latitude_of_projection_origin = _validate_map_parameter( + "latitude_of_projection_origin", latitude_of_projection_origin + ) + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) diff --git a/cf/gridmappings/abstract/conic.py b/cf/gridmappings/abstract/conic.py new file mode 100644 index 0000000000..0c1989d947 --- /dev/null +++ b/cf/gridmappings/abstract/conic.py @@ -0,0 +1,85 @@ +from .gridmappingbase import ( + GridMapping, + _validate_map_parameter, +) + + +class ConicGridMapping(GridMapping): + """A Grid Mapping with Conic classification. + + .. versionadded:: GMVER + + :Parameters: + + standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being set for either. + + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. + + The default is (0.0, 0.0), that is 0.0 degrees_north + for the first and second standard parallel values. + + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + def __init__( + self, + standard_parallel, + longitude_of_central_meridian=0.0, + latitude_of_projection_origin=0.0, + false_easting=0.0, + false_northing=0.0, + **kwargs, + ): + super().__init__(**kwargs) + + self.standard_parallel = ( + _validate_map_parameter("standard_parallel", standard_parallel[0]), + _validate_map_parameter("standard_parallel", standard_parallel[1]), + ) + self.longitude_of_central_meridian = _validate_map_parameter( + "longitude_of_central_meridian", longitude_of_central_meridian + ) + self.latitude_of_projection_origin = _validate_map_parameter( + "latitude_of_projection_origin", latitude_of_projection_origin + ) + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) diff --git a/cf/gridmappings/abstract/cylindrical.py b/cf/gridmappings/abstract/cylindrical.py new file mode 100644 index 0000000000..47e44ee02a --- /dev/null +++ b/cf/gridmappings/abstract/cylindrical.py @@ -0,0 +1,38 @@ +from .gridmappingbase import ( + GridMapping, + _validate_map_parameter, +) + + +class CylindricalGridMapping(GridMapping): + """A Grid Mapping with Cylindrical classification. + + .. versionadded:: GMVER + + :Parameters: + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + def __init__(self, false_easting=0.0, false_northing=0.0, **kwargs): + super().__init__(**kwargs) + + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) diff --git a/cf/gridmappings/abstract/latlon.py b/cf/gridmappings/abstract/latlon.py new file mode 100644 index 0000000000..44381df9c4 --- /dev/null +++ b/cf/gridmappings/abstract/latlon.py @@ -0,0 +1,18 @@ +from .gridmappingbase import ( + GridMapping, + _validate_map_parameter, +) + + +class LatLonGridMapping(GridMapping): + """A Grid Mapping with Latitude-Longitude nature. + + Such a Grid Mapping is based upon latitude and longitude coordinates + on a spherical Earth, defining the canonical 2D geographical coordinate + system so that the figure of the Earth can be described. + + .. versionadded:: GMVER + + """ + + pass diff --git a/cf/gridmappings/abstract/perspective.py b/cf/gridmappings/abstract/perspective.py new file mode 100644 index 0000000000..0a3bd38a16 --- /dev/null +++ b/cf/gridmappings/abstract/perspective.py @@ -0,0 +1,27 @@ +from .gridmappingbase import _validate_map_parameter +from .azimuthal import AzimuthalGridMapping + + +class PerspectiveGridMapping(AzimuthalGridMapping): + """A Grid Mapping with Azimuthal classification and perspective view. + + .. versionadded:: GMVER + + :Parameters: + + perspective_point_height: number or scalar `Data` + The height of the view point above the surface (PROJ + 'h') value, for example the height of a satellite above + the Earth, in units of meters 'm'. If provided + as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + + """ + + def __init__(self, perspective_point_height, **kwargs): + super().__init__(**kwargs) + + self.perspective_point_height = _validate_map_parameter( + "perspective_point_height", perspective_point_height + ) diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index cce273154a..ffa0275dda 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -16,9 +16,6 @@ CylindricalGridMapping, LatLonGridMapping, PerspectiveGridMapping, - _all_abstract_grid_mappings, - convert_proj_angular_data_to_cf, - convert_cf_angular_data_to_proj, ) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 3321f687da..f0629a132f 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -17,6 +17,16 @@ pass +_all_abstract_grid_mappings = ( + cf.GridMapping, + cf.AzimuthalGridMapping, + cf.ConicGridMapping, + cf.CylindricalGridMapping, + cf.LatLonGridMapping, + cf.PerspectiveGridMapping, +) + + # These are those of the above which have required positional arguments all_concrete_grid_mappings_req_args = { "AlbersEqualArea": {"standard_parallel": (0.0, None)}, From 5db1f355c4c3624d0d5f01f3a0bc6606cdd3184d Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 9 Oct 2023 14:24:46 +0100 Subject: [PATCH 76/97] Separate concrete GM classes out into individual modules --- cf/gridmappings/__init__.py | 41 +- cf/gridmappings/albersequalarea.py | 66 + cf/gridmappings/azimuthalequidistant.py | 54 + cf/gridmappings/geostationary.py | 120 ++ cf/gridmappings/gridmapping.py | 1235 ----------------- cf/gridmappings/lambertazimuthalequalarea.py | 54 + cf/gridmappings/lambertconformalconic.py | 69 + .../lambertcylindricalequalarea.py | 94 ++ cf/gridmappings/latitudelongitude.py | 20 + cf/gridmappings/mercator.py | 94 ++ cf/gridmappings/obliquemercator.py | 96 ++ cf/gridmappings/orthographic.py | 54 + cf/gridmappings/polarstereographic.py | 126 ++ cf/gridmappings/rotatedlatitudelongitude.py | 61 + cf/gridmappings/sinusoidal.py | 66 + cf/gridmappings/stereographic.py | 80 ++ cf/gridmappings/transversemercator.py | 84 ++ cf/gridmappings/verticalperspective.py | 62 + 18 files changed, 1222 insertions(+), 1254 deletions(-) create mode 100644 cf/gridmappings/albersequalarea.py create mode 100644 cf/gridmappings/azimuthalequidistant.py create mode 100644 cf/gridmappings/geostationary.py create mode 100644 cf/gridmappings/lambertazimuthalequalarea.py create mode 100644 cf/gridmappings/lambertconformalconic.py create mode 100644 cf/gridmappings/lambertcylindricalequalarea.py create mode 100644 cf/gridmappings/latitudelongitude.py create mode 100644 cf/gridmappings/mercator.py create mode 100644 cf/gridmappings/obliquemercator.py create mode 100644 cf/gridmappings/orthographic.py create mode 100644 cf/gridmappings/polarstereographic.py create mode 100644 cf/gridmappings/rotatedlatitudelongitude.py create mode 100644 cf/gridmappings/sinusoidal.py create mode 100644 cf/gridmappings/stereographic.py create mode 100644 cf/gridmappings/transversemercator.py create mode 100644 cf/gridmappings/verticalperspective.py diff --git a/cf/gridmappings/__init__.py b/cf/gridmappings/__init__.py index ad087c3f89..61de4f1a15 100644 --- a/cf/gridmappings/__init__.py +++ b/cf/gridmappings/__init__.py @@ -16,22 +16,25 @@ """ -from .gridmapping import ( - AlbersEqualArea, - AzimuthalEquidistant, - Geostationary, - LambertAzimuthalEqualArea, - LambertConformalConic, - LambertCylindricalEqualArea, - Mercator, - ObliqueMercator, - Orthographic, - PolarStereographic, - RotatedLatitudeLongitude, - LatitudeLongitude, - Sinusoidal, - Stereographic, - TransverseMercator, - VerticalPerspective, - _all_concrete_grid_mappings, -) + +# Concrete classes for all Grid Mappings supported by the CF Conventions. +# For the full listing, see: +# https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ +# cf-conventions.html#appendix-grid-mappings +# from which these classes should be kept consistent and up-to-date. +from .albersequalarea import AlbersEqualArea +from .azimuthalequidistant import AzimuthalEquidistant +from .geostationary import Geostationary +from .lambertazimuthalequalarea import LambertAzimuthalEqualArea +from .lambertconformalconic import LambertConformalConic +from .lambertcylindricalequalarea import LambertCylindricalEqualArea +from .mercator import Mercator +from .obliquemercator import ObliqueMercator +from .orthographic import Orthographic +from .polarstereographic import PolarStereographic +from .rotatedlatitudelongitude import RotatedLatitudeLongitude +from .latitudelongitude import LatitudeLongitude +from .sinusoidal import Sinusoidal +from .stereographic import Stereographic +from .transversemercator import TransverseMercator +from .verticalperspective import VerticalPerspective diff --git a/cf/gridmappings/albersequalarea.py b/cf/gridmappings/albersequalarea.py new file mode 100644 index 0000000000..de2e80a58b --- /dev/null +++ b/cf/gridmappings/albersequalarea.py @@ -0,0 +1,66 @@ +from .abstract import ConicGridMapping + + +class AlbersEqualArea(ConicGridMapping): + """The Albers Equal Area grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_albers_equal_area + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/aea.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being set for either. + + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. + + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + grid_mapping_name = "albers_conical_equal_area" + proj_id = "aea" diff --git a/cf/gridmappings/azimuthalequidistant.py b/cf/gridmappings/azimuthalequidistant.py new file mode 100644 index 0000000000..7b322a27cf --- /dev/null +++ b/cf/gridmappings/azimuthalequidistant.py @@ -0,0 +1,54 @@ +from .abstract import AzimuthalGridMapping + + +class AzimuthalEquidistant(AzimuthalGridMapping): + """The Azimuthal Equidistant grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#azimuthal-equidistant + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/aeqd.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + grid_mapping_name = "azimuthal_equidistant" + proj_id = "aeqd" diff --git a/cf/gridmappings/geostationary.py b/cf/gridmappings/geostationary.py new file mode 100644 index 0000000000..82bb0ce4ff --- /dev/null +++ b/cf/gridmappings/geostationary.py @@ -0,0 +1,120 @@ +from .abstract import PerspectiveGridMapping + + +class Geostationary(PerspectiveGridMapping): + """The Geostationary Satellite View grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_geostationary_projection + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/geos.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + perspective_point_height: number or scalar `Data` + The height of the view point above the surface (PROJ + 'h') value, for example the height of a satellite above + the Earth, in units of meters 'm'. If provided + as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + sweep_angle_axis: `str`, optional + Sweep angle axis of the viewing instrument, which indicates + the axis on which the view sweeps. Valid options + are "x" and "y". The default is "y". + + For more information about the nature of this parameter, see: + + https://proj.org/en/9.2/operations/projections/ + geos.html#note-on-sweep-angle + + fixed_angle_axis: `str`, optional + The axis on which the view is fixed. It corresponds to the + inner-gimbal axis of the gimbal view model, whose axis of + rotation moves about the outer-gimbal axis. Valid options + are "x" and "y". The default is "x". + + .. note:: If the fixed_angle_axis is "x", sweep_angle_axis + is "y", and vice versa. + + """ + + grid_mapping_name = "geostationary" + proj_id = "geos" + + def __init__( + self, + perspective_point_height, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, + false_easting=0.0, + false_northing=0.0, + sweep_angle_axis="y", + fixed_angle_axis="x", + **kwargs, + ): + super().__init__( + perspective_point_height, + longitude_of_projection_origin=longitude_of_projection_origin, + latitude_of_projection_origin=latitude_of_projection_origin, + false_easting=false_easting, + false_northing=false_northing, + **kwargs, + ) + + # Values "x" and "y" are not case-sensitive, so convert to lower-case + self.sweep_angle_axis = _validate_map_parameter( + "sweep_angle_axis", sweep_angle_axis + ).lower() + self.fixed_angle_axis = _validate_map_parameter( + "fixed_angle_axis", fixed_angle_axis + ).lower() + + # sweep_angle_axis must be the opposite (of "x" and "y") to + # fixed_angle_axis. + if (self.sweep_angle_axis, self.fixed_angle_axis) not in [ + ("x", "y"), + ("y", "x"), + ]: + raise ValueError( + "The sweep_angle_axis must be the opposite value, from 'x' " + "and 'y', to the fixed_angle_axis." + ) diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index ffa0275dda..e69de29bb2 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -1,1235 +0,0 @@ -import itertools -import re -from abc import ABC, abstractmethod - -from pyproj import CRS - -from ..constants import cr_canonical_units, cr_gm_valid_attr_names_are_numeric -from ..data import Data -from ..data.utils import is_numeric_dtype -from ..units import Units - -from .abstract import ( - GridMapping, - AzimuthalGridMapping, - ConicGridMapping, - CylindricalGridMapping, - LatLonGridMapping, - PerspectiveGridMapping, -) - - -"""Concrete classes for all Grid Mappings supported by the CF Conventions. - -For the full listing, see: - -https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ -cf-conventions.html#appendix-grid-mappings - -from which these classes should be kept consistent and up-to-date. -""" - - -class AlbersEqualArea(ConicGridMapping): - """The Albers Equal Area grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_albers_equal_area - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/aea.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - standard_parallel: 2-`tuple` of number or scalar `Data` or `None` - The standard parallel value(s): the first (PROJ 'lat_1' - value) and/or the second (PROJ 'lat_2' value), given - as a 2-tuple of numbers or strings corresponding to - the first and then the second in order, where `None` - indicates that a value is not being set for either. - - If provided as a number or `Data` without units, the units - for each of the values are taken as 'degrees_north', else - the `Data` units are taken and must be angular and - compatible with latitude. - - longitude_of_central_meridian: number or scalar `Data`, optional - The longitude of (natural) origin i.e. central meridian. - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - grid_mapping_name = "albers_conical_equal_area" - proj_id = "aea" - - -class AzimuthalEquidistant(AzimuthalGridMapping): - """The Azimuthal Equidistant grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#azimuthal-equidistant - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/aeqd.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - grid_mapping_name = "azimuthal_equidistant" - proj_id = "aeqd" - - -class Geostationary(PerspectiveGridMapping): - """The Geostationary Satellite View grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_geostationary_projection - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/geos.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - perspective_point_height: number or scalar `Data` - The height of the view point above the surface (PROJ - 'h') value, for example the height of a satellite above - the Earth, in units of meters 'm'. If provided - as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - sweep_angle_axis: `str`, optional - Sweep angle axis of the viewing instrument, which indicates - the axis on which the view sweeps. Valid options - are "x" and "y". The default is "y". - - For more information about the nature of this parameter, see: - - https://proj.org/en/9.2/operations/projections/ - geos.html#note-on-sweep-angle - - fixed_angle_axis: `str`, optional - The axis on which the view is fixed. It corresponds to the - inner-gimbal axis of the gimbal view model, whose axis of - rotation moves about the outer-gimbal axis. Valid options - are "x" and "y". The default is "x". - - .. note:: If the fixed_angle_axis is "x", sweep_angle_axis - is "y", and vice versa. - - """ - - grid_mapping_name = "geostationary" - proj_id = "geos" - - def __init__( - self, - perspective_point_height, - longitude_of_projection_origin=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - sweep_angle_axis="y", - fixed_angle_axis="x", - **kwargs, - ): - super().__init__( - perspective_point_height, - longitude_of_projection_origin=longitude_of_projection_origin, - latitude_of_projection_origin=latitude_of_projection_origin, - false_easting=false_easting, - false_northing=false_northing, - **kwargs, - ) - - # Values "x" and "y" are not case-sensitive, so convert to lower-case - self.sweep_angle_axis = _validate_map_parameter( - "sweep_angle_axis", sweep_angle_axis - ).lower() - self.fixed_angle_axis = _validate_map_parameter( - "fixed_angle_axis", fixed_angle_axis - ).lower() - - # sweep_angle_axis must be the opposite (of "x" and "y") to - # fixed_angle_axis. - if (self.sweep_angle_axis, self.fixed_angle_axis) not in [ - ("x", "y"), - ("y", "x"), - ]: - raise ValueError( - "The sweep_angle_axis must be the opposite value, from 'x' " - "and 'y', to the fixed_angle_axis." - ) - - -class LambertAzimuthalEqualArea(AzimuthalGridMapping): - """The Lambert Azimuthal Equal Area grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#lambert-azimuthal-equal-area - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/laea.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - grid_mapping_name = "lambert_azimuthal_equal_area" - proj_id = "laea" - - -class LambertConformalConic(ConicGridMapping): - """The Lambert Conformal Conic grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_lambert_conformal - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/lcc.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - standard_parallel: 2-`tuple` of number or scalar `Data` or `None` - The standard parallel value(s): the first (PROJ 'lat_1' - value) and/or the second (PROJ 'lat_2' value), given - as a 2-tuple of numbers or strings corresponding to - the first and then the second in order, where `None` - indicates that a value is not being set for either. - - If provided as a number or `Data` without units, the units - for each of the values are taken as 'degrees_north', else - the `Data` units are taken and must be angular and - compatible with latitude. - - The default is (0.0, 0.0), that is 0.0 degrees_north - for the first and second standard parallel values. - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - grid_mapping_name = "lambert_conformal_conic" - proj_id = "lcc" - - -class LambertCylindricalEqualArea(CylindricalGridMapping): - """The Equal Area Cylindrical grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_lambert_cylindrical_equal_area - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/cea.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - standard_parallel: 2-`tuple` of number or scalar `Data` - or `None`, optional - The standard parallel value(s): the first (PROJ 'lat_1' - value) and/or the second (PROJ 'lat_2' value), given - as a 2-tuple of numbers or strings corresponding to - the first and then the second in order, where `None` - indicates that a value is not being set for either. - - If provided as a number or `Data` without units, the units - for each of the values are taken as 'degrees_north', else - the `Data` units are taken and must be angular and - compatible with latitude. - - The default is (0.0, 0.0), that is 0.0 degrees_north - for the first and second standard parallel values. - - longitude_of_central_meridian: number or scalar `Data`, optional - The longitude of (natural) origin i.e. central meridian. - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - scale_factor_at_projection_origin: number or scalar `Data`, optional - The scale factor used in the projection (PROJ 'k_0' value). - Unitless, so `Data` must be unitless. The default is 1.0. - - """ - - grid_mapping_name = "lambert_cylindrical_equal_area" - proj_id = "cea" - - def __init__( - self, - false_easting=0.0, - false_northing=0.0, - standard_parallel=(0.0, 0.0), - scale_factor_at_projection_origin=1.0, - longitude_of_central_meridian=0.0, - **kwargs, - ): - super().__init__( - false_easting=false_easting, - false_northing=false_northing, - **kwargs, - ) - - self.standard_parallel = ( - _validate_map_parameter("standard_parallel", standard_parallel[0]), - _validate_map_parameter("standard_parallel", standard_parallel[1]), - ) - self.longitude_of_central_meridian = _validate_map_parameter( - "longitude_of_central_meridian", longitude_of_central_meridian - ) - self.scale_factor_at_projection_origin = _validate_map_parameter( - "scale_factor_at_projection_origin", - scale_factor_at_projection_origin, - ) - - -class Mercator(CylindricalGridMapping): - """The Mercator grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_mercator - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/merc.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - standard_parallel: 2-`tuple` of number or scalar `Data` - or `None`, optional - The standard parallel value(s): the first (PROJ 'lat_1' - value) and/or the second (PROJ 'lat_2' value), given - as a 2-tuple of numbers or strings corresponding to - the first and then the second in order, where `None` - indicates that a value is not being set for either. - - If provided as a number or `Data` without units, the units - for each of the values are taken as 'degrees_north', else - the `Data` units are taken and must be angular and - compatible with latitude. - - The default is (0.0, 0.0), that is 0.0 degrees_north - for the first and second standard parallel values. - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - scale_factor_at_projection_origin: number or scalar `Data`, optional - The scale factor used in the projection (PROJ 'k_0' value). - Unitless, so `Data` must be unitless. The default is 1.0. - - """ - - grid_mapping_name = "mercator" - proj_id = "merc" - - def __init__( - self, - false_easting=0.0, - false_northing=0.0, - standard_parallel=(0.0, 0.0), - longitude_of_projection_origin=0.0, - scale_factor_at_projection_origin=1.0, - **kwargs, - ): - super().__init__( - false_easting=false_easting, - false_northing=false_northing, - **kwargs, - ) - - self.standard_parallel = ( - _validate_map_parameter("standard_parallel", standard_parallel[0]), - _validate_map_parameter("standard_parallel", standard_parallel[1]), - ) - self.longitude_of_projection_origin = _validate_map_parameter( - "longitude_of_projection_origin", longitude_of_projection_origin - ) - self.scale_factor_at_projection_origin = _validate_map_parameter( - "scale_factor_at_projection_origin", - scale_factor_at_projection_origin, - ) - - -class ObliqueMercator(CylindricalGridMapping): - """The Oblique Mercator grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_oblique_mercator - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/omerc.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - azimuth_of_central_line: number or scalar `Data`, optional - The azimuth i.e. tilt angle of the centerline clockwise - from north at the center point of the line (PROJ 'alpha' - value). If provided as a number or `Data` without units, - the units are taken as 'degrees', else the `Data` - units are taken and must be angular and compatible. - The default is 0.0 degrees. - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - scale_factor_at_projection_origin: number or scalar `Data`, optional - The scale factor used in the projection (PROJ 'k_0' value). - Unitless, so `Data` must be unitless. The default is 1.0. - - """ - - grid_mapping_name = "oblique_mercator" - proj_id = "omerc" - - def __init__( - self, - 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, - false_easting=0.0, - false_northing=0.0, - **kwargs, - ): - super().__init__( - false_easting=false_easting, - false_northing=false_northing, - **kwargs, - ) - - self.azimuth_of_central_line = _validate_map_parameter( - "azimuth_of_central_line", azimuth_of_central_line - ) - self.latitude_of_projection_origin = _validate_map_parameter( - "latitude_of_projection_origin", latitude_of_projection_origin - ) - self.longitude_of_projection_origin = _validate_map_parameter( - "longitude_of_projection_origin", longitude_of_projection_origin - ) - self.scale_factor_at_projection_origin = _validate_map_parameter( - "scale_factor_at_projection_origin", - scale_factor_at_projection_origin, - ) - - -class Orthographic(AzimuthalGridMapping): - """The Orthographic grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_orthographic - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/ortho.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - grid_mapping_name = "orthographic" - proj_id = "ortho" - - -class PolarStereographic(AzimuthalGridMapping): - """The Universal Polar Stereographic grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#polar-stereographic - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/ups.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - straight_vertical_longitude_from_pole: number or scalar `Data`, optional - The longitude of (natural) origin i.e. central meridian, - oriented straight up from the North or South Pole. - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - scale_factor_at_projection_origin: number or scalar `Data`, optional - The scale factor used in the projection (PROJ 'k_0' value). - Unitless, so `Data` must be unitless. The default is 1.0. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - standard_parallel: 2-`tuple` of number or scalar `Data` - or `None`, optional - The standard parallel value(s): the first (PROJ 'lat_1' - value) and/or the second (PROJ 'lat_2' value), given - as a 2-tuple of numbers or strings corresponding to - the first and then the second in order, where `None` - indicates that a value is not being set for either. - - If provided as a number or `Data` without units, the units - for each of the values are taken as 'degrees_north', else - the `Data` units are taken and must be angular and - compatible with latitude. - - The default is (0.0, 0.0), that is 0.0 degrees_north - for the first and second standard parallel values. - - """ - - grid_mapping_name = "polar_stereographic" - proj_id = "ups" - - def __init__( - self, - latitude_of_projection_origin=0.0, - longitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - standard_parallel=(0.0, 0.0), - straight_vertical_longitude_from_pole=0.0, - scale_factor_at_projection_origin=1.0, - **kwargs, - ): - # TODO check defaults here, they do not appear for - # CRS.from_proj4("+proj=ups").to_cf() to cross reference! - super().__init__( - latitude_of_projection_origin=latitude_of_projection_origin, - longitude_of_projection_origin=longitude_of_projection_origin, - false_easting=false_easting, - false_northing=false_northing, - **kwargs, - ) - - # See: https://github.com/cf-convention/cf-conventions/issues/445 - if ( - longitude_of_projection_origin - and straight_vertical_longitude_from_pole - ): - raise ValueError( - "Only one of 'longitude_of_projection_origin' and " - "'straight_vertical_longitude_from_pole' can be set." - ) - - self.straight_vertical_longitude_from_pole = _validate_map_parameter( - "straight_vertical_longitude_from_pole", - straight_vertical_longitude_from_pole, - ) - self.standard_parallel = ( - _validate_map_parameter("standard_parallel", standard_parallel[0]), - _validate_map_parameter("standard_parallel", standard_parallel[1]), - ) - self.scale_factor_at_projection_origin = _validate_map_parameter( - "scale_factor_at_projection_origin", - scale_factor_at_projection_origin, - ) - - -class RotatedLatitudeLongitude(LatLonGridMapping): - """The Rotated Latitude-Longitude grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_rotated_pole - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - grid_north_pole_latitude: number or scalar `Data` - Latitude of the North pole of the unrotated source CRS, - expressed in the rotated geographic CRS. - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - - grid_north_pole_longitude: number or scalar `Data` - Longitude of the North pole of the unrotated source CRS, - expressed in the rotated geographic CRS. - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - - north_pole_grid_longitude: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - - """ - - grid_mapping_name = "rotated_latitude_longitude" - proj_id = "latlong" - - def __init__( - self, - grid_north_pole_latitude, - grid_north_pole_longitude, - north_pole_grid_longitude=0.0, - **kwargs, - ): - super().__init__(**kwargs) - - self.grid_north_pole_latitude = _validate_map_parameter( - "grid_north_pole_latitude", grid_north_pole_latitude - ) - self.grid_north_pole_longitude = _validate_map_parameter( - "grid_north_pole_longitude", grid_north_pole_longitude - ) - self.north_pole_grid_longitude = _validate_map_parameter( - "north_pole_grid_longitude", north_pole_grid_longitude - ) - - -class LatitudeLongitude(LatLonGridMapping): - """The Latitude-Longitude grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_latitude_longitude - - for more information. - - .. versionadded:: GMVER - - """ - - grid_mapping_name = "latitude_longitude" - proj_id = "latlong" - - -class Sinusoidal(GridMapping): - """The Sinusoidal (Sanson-Flamsteed) grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_sinusoidal - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/sinu.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - longitude_of_central_meridian: number or scalar `Data`, optional - The longitude of (natural) origin i.e. central meridian. - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - grid_mapping_name = "sinusoidal" - proj_id = "sinu" - - def __init__( - self, - longitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - **kwargs, - ): - super().__init__(**kwargs) - - self.longitude_of_projection_origin = _validate_map_parameter( - "longitude_of_projection_origin", longitude_of_projection_origin - ) - self.false_easting = _validate_map_parameter( - "false_easting", false_easting - ) - self.false_northing = _validate_map_parameter( - "false_northing", false_northing - ) - - -class Stereographic(AzimuthalGridMapping): - """The Stereographic grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_stereographic - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/stere.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - scale_factor_at_projection_origin: number or scalar `Data`, optional - The scale factor used in the projection (PROJ 'k_0' value). - Unitless, so `Data` must be unitless. The default is 1.0. - - """ - - grid_mapping_name = "stereographic" - proj_id = "stere" - - def __init__( - self, - false_easting=0.0, - false_northing=0.0, - longitude_of_projection_origin=0.0, - latitude_of_projection_origin=0.0, - scale_factor_at_projection_origin=1.0, - **kwargs, - ): - super().__init__( - false_easting=false_easting, - false_northing=false_northing, - longitude_of_projection_origin=longitude_of_projection_origin, - latitude_of_projection_origin=latitude_of_projection_origin, - **kwargs, - ) - - self.scale_factor_at_projection_origin = _validate_map_parameter( - "scale_factor_at_projection_origin", - scale_factor_at_projection_origin, - ) - - -class TransverseMercator(CylindricalGridMapping): - """The Transverse Mercator grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#_transverse_mercator - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/tmerc.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - scale_factor_at_projection_origin: number or scalar `Data`, optional - The scale factor used in the projection (PROJ 'k_0' value). - Unitless, so `Data` must be unitless. The default is 1.0. - - longitude_of_central_meridian: number or scalar `Data`, optional - The longitude of (natural) origin i.e. central meridian. - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - """ - - grid_mapping_name = "transverse_mercator" - proj_id = "tmerc" - - def __init__( - self, - scale_factor_at_central_meridian=1.0, - longitude_of_central_meridian=0.0, - latitude_of_projection_origin=0.0, - false_easting=0.0, - false_northing=0.0, - **kwargs, - ): - super().__init__( - false_easting=false_easting, - false_northing=false_northing, - **kwargs, - ) - - self.scale_factor_at_central_meridian = _validate_map_parameter( - "scale_factor_at_central_meridian", - scale_factor_at_central_meridian, - ) - self.longitude_of_central_meridian = _validate_map_parameter( - "longitude_of_central_meridian", longitude_of_central_meridian - ) - self.latitude_of_projection_origin = _validate_map_parameter( - "latitude_of_projection_origin", latitude_of_projection_origin - ) - - -class VerticalPerspective(PerspectiveGridMapping): - """The Vertical (or Near-sided) Perspective grid mapping. - - See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on - this grid mapping: - - http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ - cf-conventions.html#vertical-perspective - - or the corresponding PROJ projection page: - - https://proj.org/en/9.2/operations/projections/nsper.html - - for more information. - - .. versionadded:: GMVER - - :Parameters: - - perspective_point_height: number or scalar `Data` - The height of the view point above the surface (PROJ - 'h') value, for example the height of a satellite above - the Earth, in units of meters 'm'. If provided - as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - - longitude_of_projection_origin: number or scalar `Data`, optional - The longitude of projection center (PROJ 'lon_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_east', else the `Data` units are - taken and must be angular and compatible with longitude. - The default is 0.0 degrees_east. - - latitude_of_projection_origin: number or scalar `Data`, optional - The latitude of projection center (PROJ 'lat_0' value). - If provided as a number or `Data` without units, the units - are taken as 'degrees_north', else the `Data` units are - taken and must be angular and compatible with latitude. - The default is 0.0 degrees_north. - - false_easting: number or scalar `Data`, optional - The false easting (PROJ 'x_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - false_northing: number or scalar `Data`, optional - The false northing (PROJ 'y_0') value. - If provided as a number or `Data` without units, the units - are taken as metres 'm', else the `Data` units are - taken and must be compatible with distance. - The default is 0.0 metres. - - """ - - grid_mapping_name = "vertical_perspective" - proj_id = "nsper" - - -# Representing all Grid Mappings repsented by the CF Conventions (Appendix F) -_all_concrete_grid_mappings = ( - AlbersEqualArea, - AzimuthalEquidistant, - Geostationary, - LambertAzimuthalEqualArea, - LambertConformalConic, - LambertCylindricalEqualArea, - Mercator, - ObliqueMercator, - Orthographic, - PolarStereographic, - RotatedLatitudeLongitude, - LatitudeLongitude, - Sinusoidal, - Stereographic, - TransverseMercator, - VerticalPerspective, -) diff --git a/cf/gridmappings/lambertazimuthalequalarea.py b/cf/gridmappings/lambertazimuthalequalarea.py new file mode 100644 index 0000000000..94b23aefb4 --- /dev/null +++ b/cf/gridmappings/lambertazimuthalequalarea.py @@ -0,0 +1,54 @@ +from .abstract import AzimuthalGridMapping + + +class LambertAzimuthalEqualArea(AzimuthalGridMapping): + """The Lambert Azimuthal Equal Area grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#lambert-azimuthal-equal-area + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/laea.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + grid_mapping_name = "lambert_azimuthal_equal_area" + proj_id = "laea" diff --git a/cf/gridmappings/lambertconformalconic.py b/cf/gridmappings/lambertconformalconic.py new file mode 100644 index 0000000000..6a0e9e4e61 --- /dev/null +++ b/cf/gridmappings/lambertconformalconic.py @@ -0,0 +1,69 @@ +from .abstract import ConicGridMapping + + +class LambertConformalConic(ConicGridMapping): + """The Lambert Conformal Conic grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_lambert_conformal + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/lcc.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + standard_parallel: 2-`tuple` of number or scalar `Data` or `None` + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being set for either. + + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. + + The default is (0.0, 0.0), that is 0.0 degrees_north + for the first and second standard parallel values. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + grid_mapping_name = "lambert_conformal_conic" + proj_id = "lcc" diff --git a/cf/gridmappings/lambertcylindricalequalarea.py b/cf/gridmappings/lambertcylindricalequalarea.py new file mode 100644 index 0000000000..33c246a39b --- /dev/null +++ b/cf/gridmappings/lambertcylindricalequalarea.py @@ -0,0 +1,94 @@ +from .abstract import CylindricalGridMapping + + +class LambertCylindricalEqualArea(CylindricalGridMapping): + """The Equal Area Cylindrical grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_lambert_cylindrical_equal_area + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/cea.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + standard_parallel: 2-`tuple` of number or scalar `Data` + or `None`, optional + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being set for either. + + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. + + The default is (0.0, 0.0), that is 0.0 degrees_north + for the first and second standard parallel values. + + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + scale_factor_at_projection_origin: number or scalar `Data`, optional + The scale factor used in the projection (PROJ 'k_0' value). + Unitless, so `Data` must be unitless. The default is 1.0. + + """ + + grid_mapping_name = "lambert_cylindrical_equal_area" + proj_id = "cea" + + def __init__( + self, + false_easting=0.0, + false_northing=0.0, + standard_parallel=(0.0, 0.0), + scale_factor_at_projection_origin=1.0, + longitude_of_central_meridian=0.0, + **kwargs, + ): + super().__init__( + false_easting=false_easting, + false_northing=false_northing, + **kwargs, + ) + + self.standard_parallel = ( + _validate_map_parameter("standard_parallel", standard_parallel[0]), + _validate_map_parameter("standard_parallel", standard_parallel[1]), + ) + self.longitude_of_central_meridian = _validate_map_parameter( + "longitude_of_central_meridian", longitude_of_central_meridian + ) + self.scale_factor_at_projection_origin = _validate_map_parameter( + "scale_factor_at_projection_origin", + scale_factor_at_projection_origin, + ) diff --git a/cf/gridmappings/latitudelongitude.py b/cf/gridmappings/latitudelongitude.py new file mode 100644 index 0000000000..eb3ffd7f75 --- /dev/null +++ b/cf/gridmappings/latitudelongitude.py @@ -0,0 +1,20 @@ +from .abstract import LatLonGridMapping + + +class LatitudeLongitude(LatLonGridMapping): + """The Latitude-Longitude grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_latitude_longitude + + for more information. + + .. versionadded:: GMVER + + """ + + grid_mapping_name = "latitude_longitude" + proj_id = "latlong" diff --git a/cf/gridmappings/mercator.py b/cf/gridmappings/mercator.py new file mode 100644 index 0000000000..9d9a63b262 --- /dev/null +++ b/cf/gridmappings/mercator.py @@ -0,0 +1,94 @@ +from .abstract import CylindricalGridMapping + + +class Mercator(CylindricalGridMapping): + """The Mercator grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_mercator + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/merc.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + standard_parallel: 2-`tuple` of number or scalar `Data` + or `None`, optional + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being set for either. + + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. + + The default is (0.0, 0.0), that is 0.0 degrees_north + for the first and second standard parallel values. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + scale_factor_at_projection_origin: number or scalar `Data`, optional + The scale factor used in the projection (PROJ 'k_0' value). + Unitless, so `Data` must be unitless. The default is 1.0. + + """ + + grid_mapping_name = "mercator" + proj_id = "merc" + + def __init__( + self, + false_easting=0.0, + false_northing=0.0, + standard_parallel=(0.0, 0.0), + longitude_of_projection_origin=0.0, + scale_factor_at_projection_origin=1.0, + **kwargs, + ): + super().__init__( + false_easting=false_easting, + false_northing=false_northing, + **kwargs, + ) + + self.standard_parallel = ( + _validate_map_parameter("standard_parallel", standard_parallel[0]), + _validate_map_parameter("standard_parallel", standard_parallel[1]), + ) + self.longitude_of_projection_origin = _validate_map_parameter( + "longitude_of_projection_origin", longitude_of_projection_origin + ) + self.scale_factor_at_projection_origin = _validate_map_parameter( + "scale_factor_at_projection_origin", + scale_factor_at_projection_origin, + ) diff --git a/cf/gridmappings/obliquemercator.py b/cf/gridmappings/obliquemercator.py new file mode 100644 index 0000000000..f249c7e02d --- /dev/null +++ b/cf/gridmappings/obliquemercator.py @@ -0,0 +1,96 @@ +from .abstract import CylindricalGridMapping + + +class ObliqueMercator(CylindricalGridMapping): + """The Oblique Mercator grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_oblique_mercator + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/omerc.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + azimuth_of_central_line: number or scalar `Data`, optional + The azimuth i.e. tilt angle of the centerline clockwise + from north at the center point of the line (PROJ 'alpha' + value). If provided as a number or `Data` without units, + the units are taken as 'degrees', else the `Data` + units are taken and must be angular and compatible. + The default is 0.0 degrees. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + scale_factor_at_projection_origin: number or scalar `Data`, optional + The scale factor used in the projection (PROJ 'k_0' value). + Unitless, so `Data` must be unitless. The default is 1.0. + + """ + + grid_mapping_name = "oblique_mercator" + proj_id = "omerc" + + def __init__( + self, + 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, + false_easting=0.0, + false_northing=0.0, + **kwargs, + ): + super().__init__( + false_easting=false_easting, + false_northing=false_northing, + **kwargs, + ) + + self.azimuth_of_central_line = _validate_map_parameter( + "azimuth_of_central_line", azimuth_of_central_line + ) + self.latitude_of_projection_origin = _validate_map_parameter( + "latitude_of_projection_origin", latitude_of_projection_origin + ) + self.longitude_of_projection_origin = _validate_map_parameter( + "longitude_of_projection_origin", longitude_of_projection_origin + ) + self.scale_factor_at_projection_origin = _validate_map_parameter( + "scale_factor_at_projection_origin", + scale_factor_at_projection_origin, + ) diff --git a/cf/gridmappings/orthographic.py b/cf/gridmappings/orthographic.py new file mode 100644 index 0000000000..0ef0eea14a --- /dev/null +++ b/cf/gridmappings/orthographic.py @@ -0,0 +1,54 @@ +from .abstract import AzimuthalGridMapping + + +class Orthographic(AzimuthalGridMapping): + """The Orthographic grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_orthographic + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/ortho.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + grid_mapping_name = "orthographic" + proj_id = "ortho" diff --git a/cf/gridmappings/polarstereographic.py b/cf/gridmappings/polarstereographic.py new file mode 100644 index 0000000000..9ef821df52 --- /dev/null +++ b/cf/gridmappings/polarstereographic.py @@ -0,0 +1,126 @@ +from .abstract import AzimuthalGridMapping + + +class PolarStereographic(AzimuthalGridMapping): + """The Universal Polar Stereographic grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#polar-stereographic + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/ups.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + straight_vertical_longitude_from_pole: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian, + oriented straight up from the North or South Pole. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + scale_factor_at_projection_origin: number or scalar `Data`, optional + The scale factor used in the projection (PROJ 'k_0' value). + Unitless, so `Data` must be unitless. The default is 1.0. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + standard_parallel: 2-`tuple` of number or scalar `Data` + or `None`, optional + The standard parallel value(s): the first (PROJ 'lat_1' + value) and/or the second (PROJ 'lat_2' value), given + as a 2-tuple of numbers or strings corresponding to + the first and then the second in order, where `None` + indicates that a value is not being set for either. + + If provided as a number or `Data` without units, the units + for each of the values are taken as 'degrees_north', else + the `Data` units are taken and must be angular and + compatible with latitude. + + The default is (0.0, 0.0), that is 0.0 degrees_north + for the first and second standard parallel values. + + """ + + grid_mapping_name = "polar_stereographic" + proj_id = "ups" + + def __init__( + self, + latitude_of_projection_origin=0.0, + longitude_of_projection_origin=0.0, + false_easting=0.0, + false_northing=0.0, + standard_parallel=(0.0, 0.0), + straight_vertical_longitude_from_pole=0.0, + scale_factor_at_projection_origin=1.0, + **kwargs, + ): + # TODO check defaults here, they do not appear for + # CRS.from_proj4("+proj=ups").to_cf() to cross reference! + super().__init__( + latitude_of_projection_origin=latitude_of_projection_origin, + longitude_of_projection_origin=longitude_of_projection_origin, + false_easting=false_easting, + false_northing=false_northing, + **kwargs, + ) + + # See: https://github.com/cf-convention/cf-conventions/issues/445 + if ( + longitude_of_projection_origin + and straight_vertical_longitude_from_pole + ): + raise ValueError( + "Only one of 'longitude_of_projection_origin' and " + "'straight_vertical_longitude_from_pole' can be set." + ) + + self.straight_vertical_longitude_from_pole = _validate_map_parameter( + "straight_vertical_longitude_from_pole", + straight_vertical_longitude_from_pole, + ) + self.standard_parallel = ( + _validate_map_parameter("standard_parallel", standard_parallel[0]), + _validate_map_parameter("standard_parallel", standard_parallel[1]), + ) + self.scale_factor_at_projection_origin = _validate_map_parameter( + "scale_factor_at_projection_origin", + scale_factor_at_projection_origin, + ) diff --git a/cf/gridmappings/rotatedlatitudelongitude.py b/cf/gridmappings/rotatedlatitudelongitude.py new file mode 100644 index 0000000000..fbe7292920 --- /dev/null +++ b/cf/gridmappings/rotatedlatitudelongitude.py @@ -0,0 +1,61 @@ +from .abstract import LatLonGridMapping + + +class RotatedLatitudeLongitude(LatLonGridMapping): + """The Rotated Latitude-Longitude grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_rotated_pole + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + grid_north_pole_latitude: number or scalar `Data` + Latitude of the North pole of the unrotated source CRS, + expressed in the rotated geographic CRS. + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + + grid_north_pole_longitude: number or scalar `Data` + Longitude of the North pole of the unrotated source CRS, + expressed in the rotated geographic CRS. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + + north_pole_grid_longitude: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + + """ + + grid_mapping_name = "rotated_latitude_longitude" + proj_id = "latlong" + + def __init__( + self, + grid_north_pole_latitude, + grid_north_pole_longitude, + north_pole_grid_longitude=0.0, + **kwargs, + ): + super().__init__(**kwargs) + + self.grid_north_pole_latitude = _validate_map_parameter( + "grid_north_pole_latitude", grid_north_pole_latitude + ) + self.grid_north_pole_longitude = _validate_map_parameter( + "grid_north_pole_longitude", grid_north_pole_longitude + ) + self.north_pole_grid_longitude = _validate_map_parameter( + "north_pole_grid_longitude", north_pole_grid_longitude + ) diff --git a/cf/gridmappings/sinusoidal.py b/cf/gridmappings/sinusoidal.py new file mode 100644 index 0000000000..8060e40c22 --- /dev/null +++ b/cf/gridmappings/sinusoidal.py @@ -0,0 +1,66 @@ +from .abstract import GridMapping + + +class Sinusoidal(GridMapping): + """The Sinusoidal (Sanson-Flamsteed) grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_sinusoidal + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/sinu.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + grid_mapping_name = "sinusoidal" + proj_id = "sinu" + + def __init__( + self, + longitude_of_projection_origin=0.0, + false_easting=0.0, + false_northing=0.0, + **kwargs, + ): + super().__init__(**kwargs) + + self.longitude_of_projection_origin = _validate_map_parameter( + "longitude_of_projection_origin", longitude_of_projection_origin + ) + self.false_easting = _validate_map_parameter( + "false_easting", false_easting + ) + self.false_northing = _validate_map_parameter( + "false_northing", false_northing + ) diff --git a/cf/gridmappings/stereographic.py b/cf/gridmappings/stereographic.py new file mode 100644 index 0000000000..c1c1bc8aad --- /dev/null +++ b/cf/gridmappings/stereographic.py @@ -0,0 +1,80 @@ +from .abstract import AzimuthalGridMapping + + +class Stereographic(AzimuthalGridMapping): + """The Stereographic grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_stereographic + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/stere.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + scale_factor_at_projection_origin: number or scalar `Data`, optional + The scale factor used in the projection (PROJ 'k_0' value). + Unitless, so `Data` must be unitless. The default is 1.0. + + """ + + grid_mapping_name = "stereographic" + proj_id = "stere" + + def __init__( + self, + false_easting=0.0, + false_northing=0.0, + longitude_of_projection_origin=0.0, + latitude_of_projection_origin=0.0, + scale_factor_at_projection_origin=1.0, + **kwargs, + ): + super().__init__( + false_easting=false_easting, + false_northing=false_northing, + longitude_of_projection_origin=longitude_of_projection_origin, + latitude_of_projection_origin=latitude_of_projection_origin, + **kwargs, + ) + + self.scale_factor_at_projection_origin = _validate_map_parameter( + "scale_factor_at_projection_origin", + scale_factor_at_projection_origin, + ) diff --git a/cf/gridmappings/transversemercator.py b/cf/gridmappings/transversemercator.py new file mode 100644 index 0000000000..3b322b26e2 --- /dev/null +++ b/cf/gridmappings/transversemercator.py @@ -0,0 +1,84 @@ +from .abstract import CylindricalGridMapping + + +class TransverseMercator(CylindricalGridMapping): + """The Transverse Mercator grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#_transverse_mercator + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/tmerc.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + scale_factor_at_projection_origin: number or scalar `Data`, optional + The scale factor used in the projection (PROJ 'k_0' value). + Unitless, so `Data` must be unitless. The default is 1.0. + + longitude_of_central_meridian: number or scalar `Data`, optional + The longitude of (natural) origin i.e. central meridian. + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + """ + + grid_mapping_name = "transverse_mercator" + proj_id = "tmerc" + + def __init__( + self, + scale_factor_at_central_meridian=1.0, + longitude_of_central_meridian=0.0, + latitude_of_projection_origin=0.0, + false_easting=0.0, + false_northing=0.0, + **kwargs, + ): + super().__init__( + false_easting=false_easting, + false_northing=false_northing, + **kwargs, + ) + + self.scale_factor_at_central_meridian = _validate_map_parameter( + "scale_factor_at_central_meridian", + scale_factor_at_central_meridian, + ) + self.longitude_of_central_meridian = _validate_map_parameter( + "longitude_of_central_meridian", longitude_of_central_meridian + ) + self.latitude_of_projection_origin = _validate_map_parameter( + "latitude_of_projection_origin", latitude_of_projection_origin + ) diff --git a/cf/gridmappings/verticalperspective.py b/cf/gridmappings/verticalperspective.py new file mode 100644 index 0000000000..0050d63b1e --- /dev/null +++ b/cf/gridmappings/verticalperspective.py @@ -0,0 +1,62 @@ +from .abstract import PerspectiveGridMapping + + +class VerticalPerspective(PerspectiveGridMapping): + """The Vertical (or Near-sided) Perspective grid mapping. + + See the CF Conventions document 'Appendix F: Grid Mappings' sub-section on + this grid mapping: + + http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ + cf-conventions.html#vertical-perspective + + or the corresponding PROJ projection page: + + https://proj.org/en/9.2/operations/projections/nsper.html + + for more information. + + .. versionadded:: GMVER + + :Parameters: + + perspective_point_height: number or scalar `Data` + The height of the view point above the surface (PROJ + 'h') value, for example the height of a satellite above + the Earth, in units of meters 'm'. If provided + as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + + longitude_of_projection_origin: number or scalar `Data`, optional + The longitude of projection center (PROJ 'lon_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_east', else the `Data` units are + taken and must be angular and compatible with longitude. + The default is 0.0 degrees_east. + + latitude_of_projection_origin: number or scalar `Data`, optional + The latitude of projection center (PROJ 'lat_0' value). + If provided as a number or `Data` without units, the units + are taken as 'degrees_north', else the `Data` units are + taken and must be angular and compatible with latitude. + The default is 0.0 degrees_north. + + false_easting: number or scalar `Data`, optional + The false easting (PROJ 'x_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + false_northing: number or scalar `Data`, optional + The false northing (PROJ 'y_0') value. + If provided as a number or `Data` without units, the units + are taken as metres 'm', else the `Data` units are + taken and must be compatible with distance. + The default is 0.0 metres. + + """ + + grid_mapping_name = "vertical_perspective" + proj_id = "nsper" From 1700b9102514aba54bec0025cfaf3b7889d511e5 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 9 Oct 2023 15:35:03 +0100 Subject: [PATCH 77/97] Tweak imports to get tests working with new module structure --- cf/__init__.py | 4 ++++ cf/domain.py | 3 +-- cf/gridmappings/__init__.py | 2 ++ cf/test/test_gridmappings.py | 24 +++++++++++++++++++++--- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cf/__init__.py b/cf/__init__.py index c5fca32f7a..712304384f 100644 --- a/cf/__init__.py +++ b/cf/__init__.py @@ -254,6 +254,9 @@ from .domainaxis import DomainAxis from .fieldancillary import FieldAncillary from .field import Field + +from .gridmappings import * + from .data import Data from .data.array import ( CFANetCDFArray, @@ -324,6 +327,7 @@ from .regrid import RegridOperator + # Set up basic logging for the full project with a root logger import logging import sys diff --git a/cf/domain.py b/cf/domain.py index a404f7ad3d..8a39c7f178 100644 --- a/cf/domain.py +++ b/cf/domain.py @@ -18,7 +18,6 @@ indices_shape, parse_indices, ) -from .gridmappings import _get_cf_grid_mapping_from_name _empty_set = set() @@ -628,7 +627,7 @@ def get_grid_mappings(self, as_class=False): ) if gm: if as_class: - gms[cref_name] = _get_cf_grid_mapping_from_name(gm) + pass # TODO UPDATE with class else: gms[cref_name] = gm return gms diff --git a/cf/gridmappings/__init__.py b/cf/gridmappings/__init__.py index 61de4f1a15..0975ee5286 100644 --- a/cf/gridmappings/__init__.py +++ b/cf/gridmappings/__init__.py @@ -17,6 +17,8 @@ """ +from .abstract import * + # Concrete classes for all Grid Mappings supported by the CF Conventions. # For the full listing, see: # https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index f0629a132f..06afe9a118 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -18,13 +18,31 @@ _all_abstract_grid_mappings = ( - cf.GridMapping, + #cf.GridMapping, cf.AzimuthalGridMapping, cf.ConicGridMapping, cf.CylindricalGridMapping, cf.LatLonGridMapping, cf.PerspectiveGridMapping, ) +_all_concrete_grid_mappings = ( + cf.AlbersEqualArea, + cf.AzimuthalEquidistant, + cf.Geostationary, + cf.LambertAzimuthalEqualArea, + cf.LambertConformalConic, + cf.LambertCylindricalEqualArea, + cf.Mercator, + cf.ObliqueMercator, + cf.Orthographic, + cf.PolarStereographic, + cf.RotatedLatitudeLongitude, + cf.LatitudeLongitude, + cf.Sinusoidal, + cf.Stereographic, + cf.TransverseMercator, + cf.VerticalPerspective, +) # These are those of the above which have required positional arguments @@ -108,7 +126,7 @@ class GridMappingsTest(unittest.TestCase): @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping__init__(self): """Test GridMapping object initiation.""" - for cls in cf._all_concrete_grid_mappings: + for cls in _all_concrete_grid_mappings: if cls.__name__ not in all_concrete_grid_mappings_req_args: cls() @@ -122,7 +140,7 @@ def test_grid_mapping__init__(self): @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping__repr__str__(self): """Test all means of GridMapping inspection.""" - for cls in cf._all_concrete_grid_mappings: + for cls in _all_concrete_grid_mappings: if cls.__name__ not in all_concrete_grid_mappings_req_args: g = cls() else: From ecd9fef0b97136c2b6071209b6e7e05d583e09a2 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 9 Oct 2023 16:12:00 +0100 Subject: [PATCH 78/97] Further import tweaks & removed function skips for test passes --- cf/gridmappings/geostationary.py | 1 + cf/gridmappings/lambertcylindricalequalarea.py | 1 + cf/gridmappings/mercator.py | 1 + cf/gridmappings/obliquemercator.py | 1 + cf/gridmappings/polarstereographic.py | 1 + cf/gridmappings/rotatedlatitudelongitude.py | 1 + cf/gridmappings/sinusoidal.py | 1 + cf/gridmappings/stereographic.py | 1 + cf/gridmappings/transversemercator.py | 1 + cf/test/test_gridmappings.py | 11 ++++++----- 10 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cf/gridmappings/geostationary.py b/cf/gridmappings/geostationary.py index 82bb0ce4ff..578b61a509 100644 --- a/cf/gridmappings/geostationary.py +++ b/cf/gridmappings/geostationary.py @@ -1,4 +1,5 @@ from .abstract import PerspectiveGridMapping +from .abstract.gridmappingbase import _validate_map_parameter class Geostationary(PerspectiveGridMapping): diff --git a/cf/gridmappings/lambertcylindricalequalarea.py b/cf/gridmappings/lambertcylindricalequalarea.py index 33c246a39b..06c0b3c246 100644 --- a/cf/gridmappings/lambertcylindricalequalarea.py +++ b/cf/gridmappings/lambertcylindricalequalarea.py @@ -1,4 +1,5 @@ from .abstract import CylindricalGridMapping +from .abstract.gridmappingbase import _validate_map_parameter class LambertCylindricalEqualArea(CylindricalGridMapping): diff --git a/cf/gridmappings/mercator.py b/cf/gridmappings/mercator.py index 9d9a63b262..117f5a411c 100644 --- a/cf/gridmappings/mercator.py +++ b/cf/gridmappings/mercator.py @@ -1,4 +1,5 @@ from .abstract import CylindricalGridMapping +from .abstract.gridmappingbase import _validate_map_parameter class Mercator(CylindricalGridMapping): diff --git a/cf/gridmappings/obliquemercator.py b/cf/gridmappings/obliquemercator.py index f249c7e02d..310906a0eb 100644 --- a/cf/gridmappings/obliquemercator.py +++ b/cf/gridmappings/obliquemercator.py @@ -1,4 +1,5 @@ from .abstract import CylindricalGridMapping +from .abstract.gridmappingbase import _validate_map_parameter class ObliqueMercator(CylindricalGridMapping): diff --git a/cf/gridmappings/polarstereographic.py b/cf/gridmappings/polarstereographic.py index 9ef821df52..29caad1096 100644 --- a/cf/gridmappings/polarstereographic.py +++ b/cf/gridmappings/polarstereographic.py @@ -1,4 +1,5 @@ from .abstract import AzimuthalGridMapping +from .abstract.gridmappingbase import _validate_map_parameter class PolarStereographic(AzimuthalGridMapping): diff --git a/cf/gridmappings/rotatedlatitudelongitude.py b/cf/gridmappings/rotatedlatitudelongitude.py index fbe7292920..eaba83e2eb 100644 --- a/cf/gridmappings/rotatedlatitudelongitude.py +++ b/cf/gridmappings/rotatedlatitudelongitude.py @@ -1,4 +1,5 @@ from .abstract import LatLonGridMapping +from .abstract.gridmappingbase import _validate_map_parameter class RotatedLatitudeLongitude(LatLonGridMapping): diff --git a/cf/gridmappings/sinusoidal.py b/cf/gridmappings/sinusoidal.py index 8060e40c22..9da1f93151 100644 --- a/cf/gridmappings/sinusoidal.py +++ b/cf/gridmappings/sinusoidal.py @@ -1,4 +1,5 @@ from .abstract import GridMapping +from .abstract.gridmappingbase import _validate_map_parameter class Sinusoidal(GridMapping): diff --git a/cf/gridmappings/stereographic.py b/cf/gridmappings/stereographic.py index c1c1bc8aad..69fed28481 100644 --- a/cf/gridmappings/stereographic.py +++ b/cf/gridmappings/stereographic.py @@ -1,4 +1,5 @@ from .abstract import AzimuthalGridMapping +from .abstract.gridmappingbase import _validate_map_parameter class Stereographic(AzimuthalGridMapping): diff --git a/cf/gridmappings/transversemercator.py b/cf/gridmappings/transversemercator.py index 3b322b26e2..22a952d18b 100644 --- a/cf/gridmappings/transversemercator.py +++ b/cf/gridmappings/transversemercator.py @@ -1,4 +1,5 @@ from .abstract import CylindricalGridMapping +from .abstract.gridmappingbase import _validate_map_parameter class TransverseMercator(CylindricalGridMapping): diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 06afe9a118..d6eb7f3dac 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -18,7 +18,7 @@ _all_abstract_grid_mappings = ( - #cf.GridMapping, + cf.GridMapping, cf.AzimuthalGridMapping, cf.ConicGridMapping, cf.CylindricalGridMapping, @@ -133,7 +133,7 @@ def test_grid_mapping__init__(self): # Shouldn't be able to instantiate any abstract classes, since # abstract methods grid_mapping_name and proj_id must be defined # further down in the inheritance chain to enable concrete classes. - for cls in cf._all_abstract_grid_mappings: + for cls in _all_abstract_grid_mappings: with self.assertRaises(TypeError): cls() @@ -210,9 +210,10 @@ def test_grid_mapping__get_cf_grid_mapping_from_name(self): "lambert_conformal_conic": cf.LambertConformalConic, "some_unsupported_name": None, }.items(): - self.assertEqual( - cf._get_cf_grid_mapping_from_name(gm_name), cf_gm_class - ) + pass # TODO UPDATE with class + # self.assertEqual( + # cf._get_cf_grid_mapping_from_name(gm_name), cf_gm_class + # ) def test_grid_mapping_convert_proj_angular_data_to_cf(self): """Test the 'convert_proj_angular_data_to_cf' function.""" From 08892704cfa9d14feca0193d15db1c3daf19235b Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 18 Oct 2023 19:29:04 +0100 Subject: [PATCH 79/97] Refactor GridMapping base so it is no longer an ABC --- cf/gridmappings/__init__.py | 41 +++++------ cf/gridmappings/abstract/gridmappingbase.py | 24 ++----- cf/gridmappings/gridmapping.py | 0 cf/test/test_gridmappings.py | 80 +++++++-------------- 4 files changed, 49 insertions(+), 96 deletions(-) delete mode 100644 cf/gridmappings/gridmapping.py diff --git a/cf/gridmappings/__init__.py b/cf/gridmappings/__init__.py index 0975ee5286..88eb9baf2e 100644 --- a/cf/gridmappings/__init__.py +++ b/cf/gridmappings/__init__.py @@ -18,25 +18,22 @@ from .abstract import * - -# Concrete classes for all Grid Mappings supported by the CF Conventions. -# For the full listing, see: -# https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ -# cf-conventions.html#appendix-grid-mappings -# from which these classes should be kept consistent and up-to-date. -from .albersequalarea import AlbersEqualArea -from .azimuthalequidistant import AzimuthalEquidistant -from .geostationary import Geostationary -from .lambertazimuthalequalarea import LambertAzimuthalEqualArea -from .lambertconformalconic import LambertConformalConic -from .lambertcylindricalequalarea import LambertCylindricalEqualArea -from .mercator import Mercator -from .obliquemercator import ObliqueMercator -from .orthographic import Orthographic -from .polarstereographic import PolarStereographic -from .rotatedlatitudelongitude import RotatedLatitudeLongitude -from .latitudelongitude import LatitudeLongitude -from .sinusoidal import Sinusoidal -from .stereographic import Stereographic -from .transversemercator import TransverseMercator -from .verticalperspective import VerticalPerspective +from .gridmapping import ( + GM, + AlbersEqualArea, + AzimuthalEquidistant, + Geostationary, + LambertAzimuthalEqualArea, + LambertConformalConic, + LambertCylindricalEqualArea, + Mercator, + ObliqueMercator, + Orthographic, + PolarStereographic, + RotatedLatitudeLongitude, + LatitudeLongitude, + Sinusoidal, + Stereographic, + TransverseMercator, + VerticalPerspective, +) diff --git a/cf/gridmappings/abstract/gridmappingbase.py b/cf/gridmappings/abstract/gridmappingbase.py index d7f5e2a07d..3f4056ce4f 100644 --- a/cf/gridmappings/abstract/gridmappingbase.py +++ b/cf/gridmappings/abstract/gridmappingbase.py @@ -1,6 +1,5 @@ import itertools import re -from abc import ABC, abstractmethod from pyproj import CRS @@ -338,9 +337,14 @@ def _validate_map_parameter(mp_name, mp_value): return mp_value -class GridMapping(ABC): +class GridMapping(): """A container for a Grid Mapping recognised by the CF Conventions.""" + # The value of the 'grid_mapping_name' attribute. + grid_mapping_name = None + # The PROJ projection identifier shorthand name. + proj_id = None + def __init__( self, # i.e. WGS1984_CF_ATTR_DEFAULTS["reference_ellipsoid_name"], etc. @@ -460,22 +464,6 @@ def __init__( # TODO hook this up to the CF CR self.crs_wkt = None - # TODO hook up to the CF CR generally: in part 2. - - @property - @classmethod - @abstractmethod - def grid_mapping_name(cls): - """The value of the 'grid_mapping_name' attribute.""" - return - - @property - @classmethod - @abstractmethod - def proj_id(cls): - """The PROJ projection identifier shorthand name.""" - return - def __repr__(self): """x.__repr__() <==> repr(x)""" # Report parent GridMapping class to indicate classification, diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index d6eb7f3dac..34b7172b30 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -46,15 +46,17 @@ # These are those of the above which have required positional arguments -all_concrete_grid_mappings_req_args = { +all_grid_mappings_required_args = { "AlbersEqualArea": {"standard_parallel": (0.0, None)}, + "ConicGridMapping": {"standard_parallel": (0.0, None)}, "Geostationary": {"perspective_point_height": 1000}, - "VerticalPerspective": {"perspective_point_height": 1000}, "LambertConformalConic": {"standard_parallel": (1.0, 1.0)}, + "PerspectiveGridMapping": {"perspective_point_height": 1000}, "RotatedLatitudeLongitude": { "grid_north_pole_latitude": 0.0, "grid_north_pole_longitude": 0.0, }, + "VerticalPerspective": {"perspective_point_height": 1000}, } @@ -78,73 +80,39 @@ class GridMappingsTest(unittest.TestCase): "f7": cf.RotatedLatitudeLongitude, } - # TODO: ignore the below for now, in short will create a new test file - # with a Oblique Mercator GM - # - # From a custom netCDF file with Oblique Mercator GM - # f_om = cf.read("oblique_mercator.nc") - - # Create some coordinate references with different GMs to test on: - cr_aea = cf.CoordinateReference( - coordinates=["coordA", "coordB", "coordC"], - coordinate_conversion=cf.CoordinateConversion( - parameters={ - "grid_mapping_name": "albers_conical_equal_area", - "standard_parallel": [10, 10], - "longitude_of_projection_origin": 45.0, - "false_easting": -1000, - "false_northing": 500, - } - ), - ) - cr_aea_actual_proj_string = ( - "+proj=aea +lat_1=10. +lat_2=10. +lon_0=45.0 +x_0=-1000. +y_0=-500." - ) - - cr_om = cf.CoordinateReference( - coordinates=["coordA", "coordB"], - coordinate_conversion=cf.CoordinateConversion( - parameters={ - "grid_mapping_name": "oblique_mercator", - "latitude_of_projection_origin": -22.0, - "longitude_of_projection_origin": -59.0, - "false_easting": -12500.0, - "false_northing": -12500.0, - "azimuth_of_central_line": 89.999999, - "scale_factor_at_projection_origin": 1.0, - "inverse_flattening": 0.0, - "semi_major_axis": 6371229.0, - } - ), - ) - cr_om_actual_proj_string = ( - "+proj=omerc +lat_0=-22.00 +alpha=89.999999 +lonc=-59.00 " - "+x_0=-12500. +y_0=-12500. +ellps=sphere +a=6371229. +b=6371229. " - "+units=m +no_defs" - ) - @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping__init__(self): """Test GridMapping object initiation.""" for cls in _all_concrete_grid_mappings: - if cls.__name__ not in all_concrete_grid_mappings_req_args: - cls() + if cls.__name__ not in all_grid_mappings_required_args: + g = cls() + g.grid_mapping_name + else: + example_minimal_args = all_grid_mappings_required_args[ + cls.__name__ + ] + g = cls(**example_minimal_args) + g.grid_mapping_name - # Shouldn't be able to instantiate any abstract classes, since - # abstract methods grid_mapping_name and proj_id must be defined - # further down in the inheritance chain to enable concrete classes. for cls in _all_abstract_grid_mappings: - with self.assertRaises(TypeError): - cls() + if cls.__name__ not in all_grid_mappings_required_args: + g = cls() + self.assertEqual(g.grid_mapping_name, None) + else: + example_minimal_args = all_grid_mappings_required_args[ + cls.__name__ + ] + g = cls(**example_minimal_args) + self.assertEqual(g.grid_mapping_name, None) @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping__repr__str__(self): """Test all means of GridMapping inspection.""" for cls in _all_concrete_grid_mappings: - if cls.__name__ not in all_concrete_grid_mappings_req_args: + if cls.__name__ not in all_grid_mappings_required_args: g = cls() else: - example_minimal_args = all_concrete_grid_mappings_req_args[ + example_minimal_args = all_grid_mappings_required_args[ cls.__name__ ] g = cls(**example_minimal_args) From fa7aed640a47609f30ca9d5b32abe34941f31290 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 18 Oct 2023 23:29:57 +0100 Subject: [PATCH 80/97] New GM class w/ factory method for concrete GM class creation --- cf/gridmappings/gridmapping.py | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 cf/gridmappings/gridmapping.py diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py new file mode 100644 index 0000000000..5c7c91037b --- /dev/null +++ b/cf/gridmappings/gridmapping.py @@ -0,0 +1,81 @@ +# Concrete classes for all Grid Mappings supported by the CF Conventions. +# For the full listing, see: +# https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/ +# cf-conventions.html#appendix-grid-mappings +# from which these classes should be kept consistent and up-to-date. +from .albersequalarea import AlbersEqualArea +from .azimuthalequidistant import AzimuthalEquidistant +from .geostationary import Geostationary +from .lambertazimuthalequalarea import LambertAzimuthalEqualArea +from .lambertconformalconic import LambertConformalConic +from .lambertcylindricalequalarea import LambertCylindricalEqualArea +from .latitudelongitude import LatitudeLongitude +from .mercator import Mercator +from .obliquemercator import ObliqueMercator +from .orthographic import Orthographic +from .polarstereographic import PolarStereographic +from .rotatedlatitudelongitude import RotatedLatitudeLongitude +from .sinusoidal import Sinusoidal +from .stereographic import Stereographic +from .transversemercator import TransverseMercator +from .verticalperspective import VerticalPerspective + + +class GM(): + """A validated Grid Mapping supported by the CF Conventions.""" + + def __new__(cls, *args, **kwargs): + """TODOGM.""" + if cls is GM: + name = cls.grid_mapping_name + + # TODO: once cf Python minimum is v.3.10, use the new match/case + # syntax to consolidate this long if/elif. + if not name: + pass # TODOGM raise a custom exception + elif name is "albers_conical_equal_area": + return AlbersEqualArea(*args, **kwargs) + elif name is "azimuthal_equidistant": + return AzimuthalEquidistant(*args, **kwargs) + elif name is "geostationary": + return Geostationary(*args, **kwargs) + elif name is "lambert_azimuthal_equal_area": + return LambertAzimuthalEqualArea(*args, **kwargs) + elif name is "lambert_conformal_conic": + return LambertConformalConic(*args, **kwargs) + elif name is "lambert_cylindrical_equal_area": + return LambertCylindricalEqualArea(*args, **kwargs) + elif name is "latitude_longitude": + return LatitudeLongitude(*args, **kwargs) + elif name is "mercator": + return Mercator(*args, **kwargs) + elif name is "oblique_mercator": + return ObliqueMercator(*args, **kwargs) + elif name is "orthographic": + return Orthographic(*args, **kwargs) + elif name is "polar_stereographic": + return PolarStereographic(*args, **kwargs) + elif name is "rotated_latitude_longitude": + return RotatedLatitudeLongitude(*args, **kwargs) + elif name is "sinusoidal": + return Sinusoidal(*args, **kwargs) + elif name is "stereographic": + return Stereographic(*args, **kwargs) + elif name is "transverse_mercator": + return TransverseMercator(*args, **kwargs) + elif name is "vertical_perspective": + return VerticalPerspective(*args, **kwargs) + else: + pass # TODOGM raise a custom exception + + def __init__(self, coordinate_reference): + """TODOGM.""" + pass # TODOGM + + def create_crs(): + """TODOGM.""" + pass # TODOGM + + def is_latlon_gm(): + """TODOGM.""" + pass # TODOGM From 4551beb83c6a71dd0eac106ae38dae924f766834 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Thu, 19 Oct 2023 00:04:09 +0100 Subject: [PATCH 81/97] Define new is_latlon_gm method --- cf/gridmappings/gridmapping.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index 5c7c91037b..6ac956dd14 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -77,5 +77,12 @@ def create_crs(): pass # TODOGM def is_latlon_gm(): - """TODOGM.""" - pass # TODOGM + """Whether the Grid Mapping is of LatitudeLongitude form. + + :Returns: + + `bool` + True only if the Grid Mapping is LatitudeLongitude. + + """ + return False From 6b14cc07efa7ddd1350da17edf43881afd08fbcb Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Thu, 19 Oct 2023 00:19:30 +0100 Subject: [PATCH 82/97] Replace identity checks with equality checks in GM class --- cf/gridmappings/gridmapping.py | 43 +++++++++++++--------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index 6ac956dd14..476074dede 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -33,37 +33,37 @@ def __new__(cls, *args, **kwargs): # syntax to consolidate this long if/elif. if not name: pass # TODOGM raise a custom exception - elif name is "albers_conical_equal_area": + elif name == "albers_conical_equal_area": return AlbersEqualArea(*args, **kwargs) - elif name is "azimuthal_equidistant": + elif name == "azimuthal_equidistant": return AzimuthalEquidistant(*args, **kwargs) - elif name is "geostationary": + elif name == "geostationary": return Geostationary(*args, **kwargs) - elif name is "lambert_azimuthal_equal_area": + elif name == "lambert_azimuthal_equal_area": return LambertAzimuthalEqualArea(*args, **kwargs) - elif name is "lambert_conformal_conic": + elif name == "lambert_conformal_conic": return LambertConformalConic(*args, **kwargs) - elif name is "lambert_cylindrical_equal_area": + elif name == "lambert_cylindrical_equal_area": return LambertCylindricalEqualArea(*args, **kwargs) - elif name is "latitude_longitude": + elif name == "latitude_longitude": return LatitudeLongitude(*args, **kwargs) - elif name is "mercator": + elif name == "mercator": return Mercator(*args, **kwargs) - elif name is "oblique_mercator": + elif name == "oblique_mercator": return ObliqueMercator(*args, **kwargs) - elif name is "orthographic": + elif name == "orthographic": return Orthographic(*args, **kwargs) - elif name is "polar_stereographic": + elif name == "polar_stereographic": return PolarStereographic(*args, **kwargs) - elif name is "rotated_latitude_longitude": + elif name == "rotated_latitude_longitude": return RotatedLatitudeLongitude(*args, **kwargs) - elif name is "sinusoidal": + elif name == "sinusoidal": return Sinusoidal(*args, **kwargs) - elif name is "stereographic": + elif name == "stereographic": return Stereographic(*args, **kwargs) - elif name is "transverse_mercator": + elif name == "transverse_mercator": return TransverseMercator(*args, **kwargs) - elif name is "vertical_perspective": + elif name == "vertical_perspective": return VerticalPerspective(*args, **kwargs) else: pass # TODOGM raise a custom exception @@ -75,14 +75,3 @@ def __init__(self, coordinate_reference): def create_crs(): """TODOGM.""" pass # TODOGM - - def is_latlon_gm(): - """Whether the Grid Mapping is of LatitudeLongitude form. - - :Returns: - - `bool` - True only if the Grid Mapping is LatitudeLongitude. - - """ - return False From 735ab38ef38ffc33c23d44c6fa9274489db2e622 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Thu, 19 Oct 2023 00:21:10 +0100 Subject: [PATCH 83/97] Move and redefine is_latlon_gm method to produce correct result --- cf/gridmappings/abstract/gridmappingbase.py | 11 +++++++++++ cf/gridmappings/latitudelongitude.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/cf/gridmappings/abstract/gridmappingbase.py b/cf/gridmappings/abstract/gridmappingbase.py index 3f4056ce4f..eef1c6f2e4 100644 --- a/cf/gridmappings/abstract/gridmappingbase.py +++ b/cf/gridmappings/abstract/gridmappingbase.py @@ -518,3 +518,14 @@ def has_crs_wkt(self): """ return self.crs_wkt is not None + + def is_latlon_gm(): + """Whether the Grid Mapping is of LatitudeLongitude form. + + :Returns: + + `bool` + True only if the Grid Mapping is LatitudeLongitude. + + """ + return False diff --git a/cf/gridmappings/latitudelongitude.py b/cf/gridmappings/latitudelongitude.py index eb3ffd7f75..820cab1518 100644 --- a/cf/gridmappings/latitudelongitude.py +++ b/cf/gridmappings/latitudelongitude.py @@ -18,3 +18,14 @@ class LatitudeLongitude(LatLonGridMapping): grid_mapping_name = "latitude_longitude" proj_id = "latlong" + + def is_latlon_gm(): + """Whether the Grid Mapping is of LatitudeLongitude form. + + :Returns: + + `bool` + True only if the Grid Mapping is LatitudeLongitude. + + """ + return True From 26d3e6c4d5ec32f21531c83dd41b4f2a304f3dcb Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Fri, 20 Oct 2023 14:44:56 +0100 Subject: [PATCH 84/97] Create custom exception to report invalid GMs --- cf/gridmappings/gridmapping.py | 41 +++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index 476074dede..7496842231 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -21,8 +21,47 @@ from .verticalperspective import VerticalPerspective +class InvalidGridMapping(Exception): + """Exception for a Grid Mapping which is not supported by CF. + + .. versionadded:: GMVER + + :Parameters: + + TODOGM. + + """ + + def __init__(self, grid_mapping, custom_message=None): + self.grid_mapping = grid_mapping + self.custom_message = custom_message + + def __str__(self): + grid_mapping_name = self.grid_mapping.grid_mapping_name + if self.custom_message: + return self.custom_message + elif grid_mapping_name: + return ( + f"Grid Mapping {self.grid_mapping} with grid_mapping_name " + f"{grid_mapping_name} is not supported by the CF " + "Conventions." + ) + else: + return ( + f"Grid Mapping {self.grid_mapping} missing grid_mapping_name " + "and therefore cannot be interpreted." + ) + + class GM(): - """A validated Grid Mapping supported by the CF Conventions.""" + """A validated Grid Mapping supported by the CF Conventions. + + .. versionadded:: GMVER + + :Parameters: + TODOGM. + + """ def __new__(cls, *args, **kwargs): """TODOGM.""" From 0ffd4c42c225722a439785774b4b11fbc2e4a905 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 24 Oct 2023 22:39:51 +0100 Subject: [PATCH 85/97] Make is_latlon_gm a classmethod and add its unit test --- cf/gridmappings/abstract/gridmappingbase.py | 3 ++- cf/gridmappings/latitudelongitude.py | 3 ++- cf/test/test_gridmappings.py | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cf/gridmappings/abstract/gridmappingbase.py b/cf/gridmappings/abstract/gridmappingbase.py index eef1c6f2e4..03abf38b10 100644 --- a/cf/gridmappings/abstract/gridmappingbase.py +++ b/cf/gridmappings/abstract/gridmappingbase.py @@ -519,7 +519,8 @@ def has_crs_wkt(self): """ return self.crs_wkt is not None - def is_latlon_gm(): + @classmethod + def is_latlon_gm(cls): """Whether the Grid Mapping is of LatitudeLongitude form. :Returns: diff --git a/cf/gridmappings/latitudelongitude.py b/cf/gridmappings/latitudelongitude.py index 820cab1518..4844f527c1 100644 --- a/cf/gridmappings/latitudelongitude.py +++ b/cf/gridmappings/latitudelongitude.py @@ -19,7 +19,8 @@ class LatitudeLongitude(LatLonGridMapping): grid_mapping_name = "latitude_longitude" proj_id = "latlong" - def is_latlon_gm(): + @classmethod + def is_latlon_gm(cls): """Whether the Grid Mapping is of LatitudeLongitude form. :Returns: diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 34b7172b30..d84efd377c 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -141,6 +141,20 @@ def test_grid_mapping__repr__str__(self): str(g4), "" ) + @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") + def test_grid_mapping_is_latlon_gm(self): + """Test the 'is_latlon_gm' method on all GridMappings.""" + # In this one case we expect True... + # TODOGM: what about cf.RotatedLatitudeLongitude? + g = cf.LatitudeLongitude + self.assertTrue(g.is_latlon_gm()) # check on class + self.assertTrue(g().is_latlon_gm()) # check on instance + + # ...and expect False for all other GridMappings + for cls in _all_concrete_grid_mappings: + if not issubclass(cls, cf.LatitudeLongitude): + self.assertFalse(cls.is_latlon_gm()) + @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping_map_parameter_validation(self): """Test the validation of map parameters to Grid Mapping classes.""" From 52fa61868b53685e542ac9867c3a209f0c086a5c Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 25 Oct 2023 00:11:49 +0100 Subject: [PATCH 86/97] Tidy validation of generic map parameters --- cf/gridmappings/abstract/gridmappingbase.py | 35 +++++++++------------ cf/test/test_gridmappings.py | 2 +- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/cf/gridmappings/abstract/gridmappingbase.py b/cf/gridmappings/abstract/gridmappingbase.py index 03abf38b10..7324fdb80b 100644 --- a/cf/gridmappings/abstract/gridmappingbase.py +++ b/cf/gridmappings/abstract/gridmappingbase.py @@ -433,12 +433,10 @@ def __init__( takes precedence. """ - # Validate the arbitary kwargs - for kwarg, value in kwargs.items(): - _validate_map_parameter(kwarg, value) - - # The attributes which describe the ellipsoid and prime meridian, - # which may be included, when applicable, with any grid mapping. + # Validate map parameters that are valid for any GridMapping. + # These are attributes which describe the ellipsoid and prime meridian, + # which may be included, when applicable, with any grid mapping, as + # specified in Appendix F of the Conventions. self.earth_radius = _validate_map_parameter( "earth_radius", earth_radius ) @@ -461,8 +459,17 @@ def __init__( "semi_minor_axis", semi_minor_axis ) - # TODO hook this up to the CF CR - self.crs_wkt = None + @classmethod + def is_latlon_gm(cls): + """Whether the Grid Mapping is of LatitudeLongitude form. + + :Returns: + + `bool` + True only if the Grid Mapping is LatitudeLongitude. + + """ + return False def __repr__(self): """x.__repr__() <==> repr(x)""" @@ -518,15 +525,3 @@ def has_crs_wkt(self): """ return self.crs_wkt is not None - - @classmethod - def is_latlon_gm(cls): - """Whether the Grid Mapping is of LatitudeLongitude form. - - :Returns: - - `bool` - True only if the Grid Mapping is LatitudeLongitude. - - """ - return False diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index d84efd377c..7bd6b06e03 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -32,12 +32,12 @@ cf.LambertAzimuthalEqualArea, cf.LambertConformalConic, cf.LambertCylindricalEqualArea, + cf.LatitudeLongitude, cf.Mercator, cf.ObliqueMercator, cf.Orthographic, cf.PolarStereographic, cf.RotatedLatitudeLongitude, - cf.LatitudeLongitude, cf.Sinusoidal, cf.Stereographic, cf.TransverseMercator, From c4e9886cbeb2e52985f78f6ab68c8e4ea79f6afc Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 25 Oct 2023 01:20:01 +0100 Subject: [PATCH 87/97] Use non-rotated LatLon GM only as dest. for create_2d_lats_and_lons --- cf/__init__.py | 2 +- cf/mixin/fielddomain.py | 14 +++++++------- cf/test/test_Domain.py | 1 + 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cf/__init__.py b/cf/__init__.py index 712304384f..8c9a767518 100644 --- a/cf/__init__.py +++ b/cf/__init__.py @@ -255,7 +255,7 @@ from .fieldancillary import FieldAncillary from .field import Field -from .gridmappings import * +from .gridmappings import * # noqa: F403 from .data import Data from .data.array import ( diff --git a/cf/mixin/fielddomain.py b/cf/mixin/fielddomain.py index 7b66db9474..9471e9f84b 100644 --- a/cf/mixin/fielddomain.py +++ b/cf/mixin/fielddomain.py @@ -2613,7 +2613,7 @@ def refs(self, *identities, **filter_kwargs): @_inplace_enabled(default=False) def create_2d_lats_and_lons( - self, destination_crs="LatLonGridMapping", inplace=False): + self, destination_crs="LatitudeLongitude", inplace=False): """Create 2-dimensional latitude and longitude coordinates. The generated coordinates are added to the `{{class}}` as new @@ -2630,10 +2630,10 @@ def create_2d_lats_and_lons( grid on which to create the latitudes and longitudes. It should be an identifier which corresponds to the name of a CF Grid Mapping class. By default, the class - taken is cf.LatLonGridMapping with "LatLonGridMapping". + taken is cf.LatitudeLongitude with "LatitudeLongitude". .. note:: Creating coordinates on a grid other than - the default cf.LatLonGridMapping is not + the default cf.LatitudeLongitude is not yet supported. {{inplace: `bool`, optional}} @@ -2648,14 +2648,14 @@ def create_2d_lats_and_lons( **Examples** - TODO + TODOGM """ - if destination_crs is not "LatLonGridMapping": + if destination_crs != "LatitudeLongitude": raise NotImplementedError( "Creating latitude and longitude coordinates for " - "a destination grid that is not described by the " - "LatLonGridMapping, the default, is not yet supported." + "a destination grid that is not described by " + "cf.LatitudeLongitude, the default, is not yet supported." ) f = _inplace_enabled_define_and_cleanup(self) diff --git a/cf/test/test_Domain.py b/cf/test/test_Domain.py index 0072c80643..181c200b0e 100644 --- a/cf/test/test_Domain.py +++ b/cf/test/test_Domain.py @@ -305,6 +305,7 @@ def test_Domain_get_grid_mappings(self): self.d.get_grid_mappings(as_class=True), { 'coordinatereference1': cf.RotatedLatitudeLongitude } + ) def test_Domain_create_regular(self): domain = cf.Domain.create_regular((-180, 180, 1), (-90, 90, 1)) From 9e92261bb404ad954daeaf1c85a91cd44deb282b Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 25 Oct 2023 01:51:48 +0100 Subject: [PATCH 88/97] Update logic of GM class to query CR construct --- cf/domain.py | 1 + cf/gridmappings/gridmapping.py | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/cf/domain.py b/cf/domain.py index 8a39c7f178..fc85e021e3 100644 --- a/cf/domain.py +++ b/cf/domain.py @@ -621,6 +621,7 @@ def get_grid_mappings(self, as_class=False): """ gms = {} + # TODOGM: what if no coordinate_conversion for cref? for cref_name, cref in self.coordinate_references().items(): gm = cref.coordinate_conversion.get_parameter( "grid_mapping_name", default=None diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index 7496842231..f46db29b74 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -22,7 +22,7 @@ class InvalidGridMapping(Exception): - """Exception for a Grid Mapping which is not supported by CF. + """Exception for a coordinate reference with unsupported Grid Mapping. .. versionadded:: GMVER @@ -32,24 +32,24 @@ class InvalidGridMapping(Exception): """ - def __init__(self, grid_mapping, custom_message=None): - self.grid_mapping = grid_mapping + def __init__(self, gm_name_attr, custom_message=None): + self.gm_name_attr = grid_mapping_name_attr self.custom_message = custom_message def __str__(self): - grid_mapping_name = self.grid_mapping.grid_mapping_name if self.custom_message: return self.custom_message - elif grid_mapping_name: + elif self.gm_name_attr: return ( - f"Grid Mapping {self.grid_mapping} with grid_mapping_name " - f"{grid_mapping_name} is not supported by the CF " - "Conventions." + f"Coordinate reference construct with grid_mapping_name " + f"{self.gm_name_attr} corresponds to a Grid Mapping that " + "is not supported by the CF Conventions." ) else: return ( - f"Grid Mapping {self.grid_mapping} missing grid_mapping_name " - "and therefore cannot be interpreted." + f"A coordinate reference construct must have an attribute " + "'Coordinate conversion:grid_mapping_name' defined in order " + "to interpret it as a GM class. Missing 'grid_mapping_name'." ) @@ -63,15 +63,17 @@ class GM(): """ - def __new__(cls, *args, **kwargs): + def __new__(cls, coordinate_reference, **kwargs): """TODOGM.""" if cls is GM: - name = cls.grid_mapping_name + name = coordinate_reference.coordinate_conversion.get_parameter( + "grid_mapping_name", default=None + ) # TODO: once cf Python minimum is v.3.10, use the new match/case # syntax to consolidate this long if/elif. if not name: - pass # TODOGM raise a custom exception + raise InvalidGridMapping(name) elif name == "albers_conical_equal_area": return AlbersEqualArea(*args, **kwargs) elif name == "azimuthal_equidistant": @@ -105,7 +107,7 @@ def __new__(cls, *args, **kwargs): elif name == "vertical_perspective": return VerticalPerspective(*args, **kwargs) else: - pass # TODOGM raise a custom exception + raise InvalidGridMapping(name) def __init__(self, coordinate_reference): """TODOGM.""" From 05ea59d5840817ed6b051433dc1607acd906fd35 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 25 Oct 2023 02:07:25 +0100 Subject: [PATCH 89/97] Add basic unit test for GM class & fix some issues highlighted --- cf/gridmappings/__init__.py | 1 + cf/gridmappings/gridmapping.py | 2 +- cf/test/test_gridmappings.py | 17 ++++++++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cf/gridmappings/__init__.py b/cf/gridmappings/__init__.py index 88eb9baf2e..1878ab2657 100644 --- a/cf/gridmappings/__init__.py +++ b/cf/gridmappings/__init__.py @@ -20,6 +20,7 @@ from .abstract import * from .gridmapping import ( GM, + InvalidGridMapping, AlbersEqualArea, AzimuthalEquidistant, Geostationary, diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index f46db29b74..444466eae6 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -33,7 +33,7 @@ class InvalidGridMapping(Exception): """ def __init__(self, gm_name_attr, custom_message=None): - self.gm_name_attr = grid_mapping_name_attr + self.gm_name_attr = gm_name_attr self.custom_message = custom_message def __str__(self): diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 7bd6b06e03..5be164dc66 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -145,7 +145,6 @@ def test_grid_mapping__repr__str__(self): def test_grid_mapping_is_latlon_gm(self): """Test the 'is_latlon_gm' method on all GridMappings.""" # In this one case we expect True... - # TODOGM: what about cf.RotatedLatitudeLongitude? g = cf.LatitudeLongitude self.assertTrue(g.is_latlon_gm()) # check on class self.assertTrue(g().is_latlon_gm()) # check on instance @@ -155,6 +154,22 @@ def test_grid_mapping_is_latlon_gm(self): if not issubclass(cls, cf.LatitudeLongitude): self.assertFalse(cls.is_latlon_gm()) + @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") + def test_grid_mapping_GM_class(self): + """Test factory creation with the GM class.""" + # Note f1 has cr0 with no GM and cr1 with 'rotated_latitude_longitude' + cr0 = self.f1.coordinate_references('coordinatereference0').value() + cr1 = self.f1.coordinate_references('coordinatereference1').value() + + # cr1 has a valid attribute defined for that + r = cf.GM(cr1) + # But cr0 has no 'grid_mapping_name' so should error + with self.assertRaises(cf.InvalidGridMapping): + cf.GM(cr0) + + + # TODOGM extend greatly! + @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping_map_parameter_validation(self): """Test the validation of map parameters to Grid Mapping classes.""" From 45387d72cc084754008b01ece580a3e6aed48a43 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 25 Oct 2023 02:20:49 +0100 Subject: [PATCH 90/97] Pass through parameters from CR construct to GM class creation --- cf/gridmappings/abstract/gridmappingbase.py | 2 +- cf/gridmappings/gridmapping.py | 49 ++++++++++++--------- cf/test/test_gridmappings.py | 7 ++- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/cf/gridmappings/abstract/gridmappingbase.py b/cf/gridmappings/abstract/gridmappingbase.py index 7324fdb80b..7cc65ec51b 100644 --- a/cf/gridmappings/abstract/gridmappingbase.py +++ b/cf/gridmappings/abstract/gridmappingbase.py @@ -511,7 +511,7 @@ def get_proj_crs(self): a `pyproj` `CRS` class that corresponds to the Grid Mapping instance. - """ + """ return CRS.from_proj4(self.get_proj_string()) def has_crs_wkt(self): diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index 444466eae6..6f09655fad 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -66,46 +66,55 @@ class GM(): def __new__(cls, coordinate_reference, **kwargs): """TODOGM.""" if cls is GM: - name = coordinate_reference.coordinate_conversion.get_parameter( + # TODOGM: what if no coordinate_conversion for cref? + cref = coordinate_reference.coordinate_conversion + name = cref.get_parameter( "grid_mapping_name", default=None ) + if not name: # Exit early before further parameter querying + raise InvalidGridMapping(name) + + cref_params = cref.parameters() + cref_params.pop("grid_mapping_name") + # Left with those parameters to describe the GM, which must be + # validated as map parameters. Add to custom kwargs + # TODOGM: care to handle duplicate kwargs from custom and cref + kwargs.update(cref_params) # TODO: once cf Python minimum is v.3.10, use the new match/case # syntax to consolidate this long if/elif. - if not name: - raise InvalidGridMapping(name) - elif name == "albers_conical_equal_area": - return AlbersEqualArea(*args, **kwargs) + if name == "albers_conical_equal_area": + return AlbersEqualArea(**kwargs) elif name == "azimuthal_equidistant": - return AzimuthalEquidistant(*args, **kwargs) + return AzimuthalEquidistant(**kwargs) elif name == "geostationary": - return Geostationary(*args, **kwargs) + return Geostationary(**kwargs) elif name == "lambert_azimuthal_equal_area": - return LambertAzimuthalEqualArea(*args, **kwargs) + return LambertAzimuthalEqualArea(**kwargs) elif name == "lambert_conformal_conic": - return LambertConformalConic(*args, **kwargs) + return LambertConformalConic(**kwargs) elif name == "lambert_cylindrical_equal_area": - return LambertCylindricalEqualArea(*args, **kwargs) + return LambertCylindricalEqualArea(**kwargs) elif name == "latitude_longitude": - return LatitudeLongitude(*args, **kwargs) + return LatitudeLongitude(**kwargs) elif name == "mercator": - return Mercator(*args, **kwargs) + return Mercator(**kwargs) elif name == "oblique_mercator": - return ObliqueMercator(*args, **kwargs) + return ObliqueMercator(**kwargs) elif name == "orthographic": - return Orthographic(*args, **kwargs) + return Orthographic(**kwargs) elif name == "polar_stereographic": - return PolarStereographic(*args, **kwargs) + return PolarStereographic(**kwargs) elif name == "rotated_latitude_longitude": - return RotatedLatitudeLongitude(*args, **kwargs) + return RotatedLatitudeLongitude(**kwargs) elif name == "sinusoidal": - return Sinusoidal(*args, **kwargs) + return Sinusoidal(**kwargs) elif name == "stereographic": - return Stereographic(*args, **kwargs) + return Stereographic(**kwargs) elif name == "transverse_mercator": - return TransverseMercator(*args, **kwargs) + return TransverseMercator(**kwargs) elif name == "vertical_perspective": - return VerticalPerspective(*args, **kwargs) + return VerticalPerspective(**kwargs) else: raise InvalidGridMapping(name) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 5be164dc66..5e362dae8f 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -163,12 +163,15 @@ def test_grid_mapping_GM_class(self): # cr1 has a valid attribute defined for that r = cf.GM(cr1) + self.assertTrue(isinstance(r, cf.RotatedLatitudeLongitude)) + self.assertEqual(r.grid_mapping_name, "rotated_latitude_longitude") # But cr0 has no 'grid_mapping_name' so should error with self.assertRaises(cf.InvalidGridMapping): cf.GM(cr0) - - # TODOGM extend greatly! + # TODOGM extend greatly - by creating CRs with more varied + # 'grid_mapping_name' attributes with corresponding valid and invalid + # parameters... @unittest.skipUnless(pyproj_imported, "Requires pyproj package.") def test_grid_mapping_map_parameter_validation(self): From 100e43c0383bdfc85920aaf8cc85fd2776b1cd34 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 25 Oct 2023 02:25:20 +0100 Subject: [PATCH 91/97] Extend GM unit test as far as current example fields can help --- cf/test/test_gridmappings.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 5e362dae8f..3f3d9a11e5 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -160,12 +160,20 @@ def test_grid_mapping_GM_class(self): # Note f1 has cr0 with no GM and cr1 with 'rotated_latitude_longitude' cr0 = self.f1.coordinate_references('coordinatereference0').value() cr1 = self.f1.coordinate_references('coordinatereference1').value() + # Note f6 has just one CR, grid mapping name of ['latitude_longitude'] + cr2 = self.f6.coordinate_references('coordinatereference0').value() - # cr1 has a valid attribute defined for that r = cf.GM(cr1) self.assertTrue(isinstance(r, cf.RotatedLatitudeLongitude)) self.assertEqual(r.grid_mapping_name, "rotated_latitude_longitude") - # But cr0 has no 'grid_mapping_name' so should error + self.assertEqual(r.grid_north_pole_latitude, 38.0) + self.assertEqual(r.grid_north_pole_longitude, 190.0) + + l = cf.GM(cr2) + self.f6.dump() + self.assertTrue(isinstance(l, cf.LatitudeLongitude)) + self.assertEqual(l.grid_mapping_name, "latitude_longitude") + with self.assertRaises(cf.InvalidGridMapping): cf.GM(cr0) From 361e4e9b35bb4efddd51504515df0e0bc0aab591 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 25 Oct 2023 02:38:25 +0100 Subject: [PATCH 92/97] Handle overriding map parameters in GM creation w/ testing --- cf/gridmappings/gridmapping.py | 14 ++++++++------ cf/test/test_gridmappings.py | 8 +++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index 6f09655fad..b3cea9a0b9 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -63,7 +63,7 @@ class GM(): """ - def __new__(cls, coordinate_reference, **kwargs): + def __new__(cls, coordinate_reference, **override_kwargs): """TODOGM.""" if cls is GM: # TODOGM: what if no coordinate_conversion for cref? @@ -74,12 +74,14 @@ def __new__(cls, coordinate_reference, **kwargs): if not name: # Exit early before further parameter querying raise InvalidGridMapping(name) - cref_params = cref.parameters() - cref_params.pop("grid_mapping_name") + kwargs = cref.parameters() + kwargs.pop("grid_mapping_name") # will be set in creation + # Left with those parameters to describe the GM, which must be - # validated as map parameters. Add to custom kwargs - # TODOGM: care to handle duplicate kwargs from custom and cref - kwargs.update(cref_params) + # validated as map parameters. Note x.update(y) will override x + # with any keys from y that are duplicates, avoiding issues + # from duplication of keyword inputs + kwargs.update(override_kwargs) # TODO: once cf Python minimum is v.3.10, use the new match/case # syntax to consolidate this long if/elif. diff --git a/cf/test/test_gridmappings.py b/cf/test/test_gridmappings.py index 3f3d9a11e5..81cddd67d8 100644 --- a/cf/test/test_gridmappings.py +++ b/cf/test/test_gridmappings.py @@ -170,12 +170,18 @@ def test_grid_mapping_GM_class(self): self.assertEqual(r.grid_north_pole_longitude, 190.0) l = cf.GM(cr2) - self.f6.dump() self.assertTrue(isinstance(l, cf.LatitudeLongitude)) self.assertEqual(l.grid_mapping_name, "latitude_longitude") with self.assertRaises(cf.InvalidGridMapping): cf.GM(cr0) + + # Test creation with overriding of parameters from the CR + r = cf.GM(cr1, **{"grid_north_pole_latitude": -45}) + self.assertTrue(isinstance(r, cf.RotatedLatitudeLongitude)) + self.assertEqual(r.grid_mapping_name, "rotated_latitude_longitude") + self.assertEqual(r.grid_north_pole_latitude, -45) + self.assertEqual(r.grid_north_pole_longitude, 190.0) # TODOGM extend greatly - by creating CRs with more varied # 'grid_mapping_name' attributes with corresponding valid and invalid From adccff5833266be818353c5b44982a6b64e571da Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Thu, 26 Oct 2023 13:16:55 +0100 Subject: [PATCH 93/97] Re-implement class GM output to reinstate test_*_get_grid_mappings --- cf/domain.py | 7 ++++++- cf/test/test_Domain.py | 7 +++++-- cf/test/test_Field.py | 10 +++++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cf/domain.py b/cf/domain.py index fc85e021e3..12165828cf 100644 --- a/cf/domain.py +++ b/cf/domain.py @@ -18,6 +18,7 @@ indices_shape, parse_indices, ) +from .gridmappings import GM, InvalidGridMapping _empty_set = set() @@ -628,7 +629,11 @@ def get_grid_mappings(self, as_class=False): ) if gm: if as_class: - pass # TODO UPDATE with class + try: + gm_class = GM(cref) + except InvalidGridMapping: + pass # not a supported GM so don't add + gms[cref_name] = gm_class else: gms[cref_name] = gm return gms diff --git a/cf/test/test_Domain.py b/cf/test/test_Domain.py index 181c200b0e..0d097a3ff5 100644 --- a/cf/test/test_Domain.py +++ b/cf/test/test_Domain.py @@ -298,12 +298,15 @@ def test_Domain_size(self): def test_Domain_get_grid_mappings(self): self.assertEqual( self.d.get_grid_mappings(), { - 'coordinatereference1': 'rotated_latitude_longitude' + "coordinatereference1": "rotated_latitude_longitude" } ) self.assertEqual( self.d.get_grid_mappings(as_class=True), { - 'coordinatereference1': cf.RotatedLatitudeLongitude + "coordinatereference1": cf.RotatedLatitudeLongitude( + grid_north_pole_latitude=38.0, + grid_north_pole_longitude=190.0, + ) } ) diff --git a/cf/test/test_Field.py b/cf/test/test_Field.py index 86b2d53016..0943e79eb6 100644 --- a/cf/test/test_Field.py +++ b/cf/test/test_Field.py @@ -2633,11 +2633,15 @@ def test_Field_auxiliary_to_dimension_to_auxiliary(self): def test_Field_get_grid_mappings(self): self.assertEqual( self.f.get_grid_mappings(), - {'coordinatereference1': 'rotated_latitude_longitude'} + {"coordinatereference1": "rotated_latitude_longitude"} ) self.assertEqual( - self.f.get_grid_mappings(as_class=True), - {'coordinatereference1': cf.RotatedLatitudeLongitude} + self.f.get_grid_mappings(as_class=True), { + "coordinatereference1": cf.RotatedLatitudeLongitude( + grid_north_pole_latitude=38.0, + grid_north_pole_longitude=190.0, + ) + } ) self.assertEqual( self.f0.get_grid_mappings(), {} From 28ca215f4ba0880bcf4185b9ef24a26424486590 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Thu, 26 Oct 2023 17:31:36 +0100 Subject: [PATCH 94/97] Declare regex as raw string to remove SyntaxWarning on import --- cf/gridmappings/abstract/gridmappingbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf/gridmappings/abstract/gridmappingbase.py b/cf/gridmappings/abstract/gridmappingbase.py index 7cc65ec51b..b0179979fd 100644 --- a/cf/gridmappings/abstract/gridmappingbase.py +++ b/cf/gridmappings/abstract/gridmappingbase.py @@ -110,7 +110,7 @@ def convert_proj_angular_data_to_cf(proj_data, context=None): # indicating decimal degrees or radians with PROJ. Be strict about an # exact regex match, because anything not following the pattern (e.g. # something with extra letters) will be ambiguous for PROJ units. - valid_form = re.compile("(-?\d+(\.\d*)?)([rRdD°]?)") + valid_form = re.compile(r"(-?\d+(\.\d*)?)([rRdD°]?)") form = re.fullmatch(valid_form, proj_data) if form: comps = form.groups() From a1a98a7ac0a6df387323ffb11c3da9dba689ecfd Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Thu, 26 Oct 2023 17:44:35 +0100 Subject: [PATCH 95/97] Fix issue w/ potential for undefied var in get_grid_mappings --- cf/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf/domain.py b/cf/domain.py index 12165828cf..24933f9efa 100644 --- a/cf/domain.py +++ b/cf/domain.py @@ -631,9 +631,9 @@ def get_grid_mappings(self, as_class=False): if as_class: try: gm_class = GM(cref) + gms[cref_name] = gm_class except InvalidGridMapping: pass # not a supported GM so don't add - gms[cref_name] = gm_class else: gms[cref_name] = gm return gms From 1cfa4b4b66883443c8a7b3498cbed76585eaf8ba Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Thu, 26 Oct 2023 18:05:03 +0100 Subject: [PATCH 96/97] Confirm logic to handle missing coordinate conversions --- cf/domain.py | 3 ++- cf/gridmappings/gridmapping.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cf/domain.py b/cf/domain.py index 24933f9efa..cf68c7a19a 100644 --- a/cf/domain.py +++ b/cf/domain.py @@ -622,8 +622,9 @@ def get_grid_mappings(self, as_class=False): """ gms = {} - # TODOGM: what if no coordinate_conversion for cref? for cref_name, cref in self.coordinate_references().items(): + # If there is no coordinate conversion or parameters set on one, + # this will give None, so is safe gm = cref.coordinate_conversion.get_parameter( "grid_mapping_name", default=None ) diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index b3cea9a0b9..5041a587a0 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -67,14 +67,14 @@ def __new__(cls, coordinate_reference, **override_kwargs): """TODOGM.""" if cls is GM: # TODOGM: what if no coordinate_conversion for cref? - cref = coordinate_reference.coordinate_conversion - name = cref.get_parameter( + conv = coordinate_reference.get_coordinate_conversion() + name = conv.get_parameter( "grid_mapping_name", default=None ) if not name: # Exit early before further parameter querying raise InvalidGridMapping(name) - kwargs = cref.parameters() + kwargs = conv.parameters() kwargs.pop("grid_mapping_name") # will be set in creation # Left with those parameters to describe the GM, which must be From 3875aab73161753c6f36695ca6d471f412a50492 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Thu, 26 Oct 2023 18:10:52 +0100 Subject: [PATCH 97/97] More comments indicating safety w/ missing coordinate conversions --- cf/gridmappings/gridmapping.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cf/gridmappings/gridmapping.py b/cf/gridmappings/gridmapping.py index 5041a587a0..65e93d809f 100644 --- a/cf/gridmappings/gridmapping.py +++ b/cf/gridmappings/gridmapping.py @@ -66,17 +66,18 @@ class GM(): def __new__(cls, coordinate_reference, **override_kwargs): """TODOGM.""" if cls is GM: - # TODOGM: what if no coordinate_conversion for cref? + # If there is no coordinate conversion or parameters set on one, + # None will result from the pair of lines below, so is safe. conv = coordinate_reference.get_coordinate_conversion() name = conv.get_parameter( "grid_mapping_name", default=None ) + if not name: # Exit early before further parameter querying raise InvalidGridMapping(name) kwargs = conv.parameters() kwargs.pop("grid_mapping_name") # will be set in creation - # Left with those parameters to describe the GM, which must be # validated as map parameters. Note x.update(y) will override x # with any keys from y that are duplicates, avoiding issues