Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Projection lat-lon coordinates including new GridMapping objects #695

Draft
wants to merge 98 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
090dae2
Add skeleton for new function in coordinatereference module
sadielbartholomew Jul 13, 2023
c716bc7
Add dict mapping grid mapping names to PROJ proj_string attrs
sadielbartholomew Jul 13, 2023
9a5353c
Add input args + form output constructs for create_2d_lats_and_lons
sadielbartholomew Jul 14, 2023
df81bb9
Update mapping dict to fill in unknowns
sadielbartholomew Jul 14, 2023
75fbceb
Create grid mapping module and classes for all supported GMs
sadielbartholomew Jul 17, 2023
1965839
Create inheritance structure for GMs by classification
sadielbartholomew Jul 17, 2023
6d60ff2
Add mapping parameters to all Grid Mapping classes
sadielbartholomew Jul 18, 2023
638b6f0
Inheritance from abstract to concrete Grid Mapping classes
sadielbartholomew Jul 18, 2023
a3a1021
Document (map) parameters for Grid Mapping classes
sadielbartholomew Jul 19, 2023
5542189
Document false_* parameters for Grid Mapping classes
sadielbartholomew Jul 19, 2023
a9e0149
Document *_at_projection_origin in Grid Mapping classes
sadielbartholomew Jul 20, 2023
a57613b
Document standard_parallel & perspective_point_height GM args
sadielbartholomew Jul 21, 2023
463dd2e
Document abstract Grid Mapping optional map parameters
sadielbartholomew Jul 21, 2023
3b3c1ca
Document Grid Mapping scale_factor_at_projection_origin parameter
sadielbartholomew Jul 31, 2023
9b41810
Document Grid Mapping longitude_of_central_meridian parameter
sadielbartholomew Jul 31, 2023
81dae17
Add default parameters from WGS 84 earth specification
sadielbartholomew Aug 1, 2023
627d65c
Set and confirm defaults from 'World Geodetic System 1984'
sadielbartholomew Aug 1, 2023
3cb45da
Update LatLon type Grid Mapping classes proj-string ID
sadielbartholomew Aug 2, 2023
f656b33
Import gridmapping classes to module and set string repr's
sadielbartholomew Aug 2, 2023
4b3ea11
Use abstract base class to improve classification of Grid Mappings
sadielbartholomew Aug 3, 2023
b684ffb
Tidy Grid Mapping class inehritance
sadielbartholomew Aug 3, 2023
37a900e
Grid Mappings: add method to return PROJ proj-string
sadielbartholomew Aug 4, 2023
e571d59
Create internal helper functions for unit conversion etc.
sadielbartholomew Aug 4, 2023
e379c52
Grid Mappings: add proj-string parameter creator function
sadielbartholomew Aug 7, 2023
84950b0
Testing: add minimal new test module test_gridmappings.py
sadielbartholomew Aug 7, 2023
9faa6fe
Add to test_gridmappings.py to prepare for GM matching
sadielbartholomew Aug 8, 2023
5ce5420
Set-up grid_mapping_name attribute value to GM class matching
sadielbartholomew Aug 8, 2023
f47a1c3
Add equality checking for Grid Mapping classes
sadielbartholomew Aug 9, 2023
5c1c7ff
Grid Mappings: add coordinate references for testing
sadielbartholomew Aug 9, 2023
751c650
Add hash for Grid Mappings classes for immutability
sadielbartholomew Aug 10, 2023
d10821e
New method get_grid_mappings() to faciliate CC parameter access
sadielbartholomew Aug 10, 2023
19856c8
Convert gm name property to class attribute
sadielbartholomew Aug 11, 2023
3f96527
Convert proj_id property to class attribute
sadielbartholomew Aug 11, 2023
23efc27
Finish units conversion in gridmappings module
sadielbartholomew Aug 14, 2023
3acae29
Complete GM-related unit conversion minus type equivalency
sadielbartholomew Aug 15, 2023
57ee19d
Apply type processing to units conversion to fix most new tests
sadielbartholomew Aug 15, 2023
b37bb60
Update logic in previous function for better unpacking
sadielbartholomew Aug 16, 2023
02aaa1a
Fix unit conversion processing of radians
sadielbartholomew Aug 16, 2023
6e60723
Include negative values as value PROJ angular values
sadielbartholomew Aug 17, 2023
b1e817d
Tidy PROJ->CF unit conversion
sadielbartholomew Aug 17, 2023
49f67c7
Add new data conversion CF->PROJ func w/ basic testing
sadielbartholomew Aug 18, 2023
c4acef3
Fix flake8 with ignore
sadielbartholomew Aug 18, 2023
4aa05c3
Add to testing of PROJ->CF Data conversions w/ inverse mapping check
sadielbartholomew Aug 22, 2023
5a027c7
Extend PROJ<->CF data test
sadielbartholomew Aug 22, 2023
84a9d21
Extend inverse relation test for PROJ<->CF data
sadielbartholomew Aug 23, 2023
cfdba78
Fix test ready for inverse PROJ<->CF data test cases
sadielbartholomew Aug 24, 2023
873ca07
Tidy convert_cf_angular_data_to_proj
sadielbartholomew Aug 24, 2023
5378645
Extend get_grid_mappings class to return classes as option
sadielbartholomew Aug 25, 2023
0a7298e
Finalise docstrings for get_grid_mapping related funcs & methods
sadielbartholomew Aug 25, 2023
70b4dce
Update test_gridmappings module docstrings
sadielbartholomew Aug 28, 2023
de888be
Extend to finalise test of CF->PROJ data
sadielbartholomew Aug 29, 2023
341e4c6
Describe ALL_GRID_MAPPING_ATTR_NAMES ready for type checking
sadielbartholomew Aug 29, 2023
b0991e5
Update constants for gm_attrs and integrate w/ constants module
sadielbartholomew Aug 30, 2023
2b61c77
Constants: CR and GM constants tidying and coordination
sadielbartholomew Aug 30, 2023
e024e7a
Update Grid Mapping parameter docs for CF unit inputs
sadielbartholomew Aug 30, 2023
73b038d
Fix old canonical units for straight_vertical_longitude_from_pole
sadielbartholomew Aug 30, 2023
723fed3
Update optional nature of standard_parallel parameter kwarg
sadielbartholomew Aug 30, 2023
d2cf1b1
Update GM comparison logic to use CRS not proj string (order dep.)
sadielbartholomew Aug 30, 2023
b6c2a66
Extend test_gridmapping GM init testing + tidy
sadielbartholomew Sep 1, 2023
5c278b8
Add map parameter validation and supply of canonical units
sadielbartholomew Sep 1, 2023
5c19217
Fix map parameter validation for standard_parallel tuple
sadielbartholomew Sep 1, 2023
3675804
Fix test_grid_mapping__repr__str__ via new valid input
sadielbartholomew Sep 1, 2023
abb2249
Add minimal test for map parameter validation and conformance
sadielbartholomew Sep 1, 2023
bda010a
Fix super call defaults to fix failing test
sadielbartholomew Sep 1, 2023
e2e2162
Fix processing of map parameters as Data
sadielbartholomew Sep 4, 2023
bd326fd
Update map parameter validation test to check unit conformance
sadielbartholomew Sep 4, 2023
2646b60
Remove read of own un-hosted netCDF test file for review
sadielbartholomew Sep 4, 2023
32ad4c2
Merge branch 'main' into projections-part-1-grid-mappings-infra
sadielbartholomew Sep 21, 2023
75dece6
Update docstring TODOs in gridmappings module
sadielbartholomew Sep 22, 2023
bfe3471
Add has_crs_wkt method to simplify CRS creation
sadielbartholomew Sep 22, 2023
bb6bf7c
Move create_2d_lats_and_lons coordinatereference -> fielddomain
sadielbartholomew Sep 26, 2023
98f73ae
Document new API for fielddomain.create_2d_lats_and_lons
sadielbartholomew Sep 26, 2023
78a4b6a
Remove functions not needed w/ change of approach
sadielbartholomew Sep 26, 2023
05d7b6d
Set up new gridmappings module ready to split code up
sadielbartholomew Sep 26, 2023
63b0f72
Set up core for gridmappings.abstract sub-sub-module
sadielbartholomew Sep 26, 2023
e446b78
Separate abstract GM classes out into individual modules
sadielbartholomew Sep 26, 2023
5db1f35
Separate concrete GM classes out into individual modules
sadielbartholomew Oct 9, 2023
1700b91
Tweak imports to get tests working with new module structure
sadielbartholomew Oct 9, 2023
ecd9fef
Further import tweaks & removed function skips for test passes
sadielbartholomew Oct 9, 2023
0889270
Refactor GridMapping base so it is no longer an ABC
sadielbartholomew Oct 18, 2023
fa7aed6
New GM class w/ factory method for concrete GM class creation
sadielbartholomew Oct 18, 2023
4551beb
Define new is_latlon_gm method
sadielbartholomew Oct 18, 2023
6b14cc0
Replace identity checks with equality checks in GM class
sadielbartholomew Oct 18, 2023
735ab38
Move and redefine is_latlon_gm method to produce correct result
sadielbartholomew Oct 18, 2023
26d3e6c
Create custom exception to report invalid GMs
sadielbartholomew Oct 20, 2023
0ffd4c4
Make is_latlon_gm a classmethod and add its unit test
sadielbartholomew Oct 24, 2023
52fa618
Tidy validation of generic map parameters
sadielbartholomew Oct 24, 2023
c4e9886
Use non-rotated LatLon GM only as dest. for create_2d_lats_and_lons
sadielbartholomew Oct 25, 2023
9e92261
Update logic of GM class to query CR construct
sadielbartholomew Oct 25, 2023
05ea59d
Add basic unit test for GM class & fix some issues highlighted
sadielbartholomew Oct 25, 2023
45387d7
Pass through parameters from CR construct to GM class creation
sadielbartholomew Oct 25, 2023
100e43c
Extend GM unit test as far as current example fields can help
sadielbartholomew Oct 25, 2023
361e4e9
Handle overriding map parameters in GM creation w/ testing
sadielbartholomew Oct 25, 2023
adccff5
Re-implement class GM output to reinstate test_*_get_grid_mappings
sadielbartholomew Oct 26, 2023
28ca215
Declare regex as raw string to remove SyntaxWarning on import
sadielbartholomew Oct 26, 2023
a1a98a7
Fix issue w/ potential for undefied var in get_grid_mappings
sadielbartholomew Oct 26, 2023
1cfa4b4
Confirm logic to handle missing coordinate conversions
sadielbartholomew Oct 26, 2023
3875aab
More comments indicating safety w/ missing coordinate conversions
sadielbartholomew Oct 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@
from .domainaxis import DomainAxis
from .fieldancillary import FieldAncillary
from .field import Field

from .gridmappings import * # noqa: F403

from .data import Data
from .data.array import (
CFANetCDFArray,
Expand Down
119 changes: 108 additions & 11 deletions cf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,26 +72,123 @@

_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:
"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"),
# ...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"),
"straight_vertical_longitude_from_pole": Units("degrees_north"),
"perspective_point_height": Units("m"),
"azimuth_of_central_line": Units("degrees"),
# 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
# 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
# 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 = {
Expand Down
54 changes: 54 additions & 0 deletions cf/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
indices_shape,
parse_indices,
)
from .gridmappings import GM, InvalidGridMapping

_empty_set = set()

Expand Down Expand Up @@ -585,6 +586,59 @@ def get_data(self, default=ValueError(), _units=None, _fill_value=True):
default, message=f"{self.__class__.__name__} has no data"
)

def get_grid_mappings(self, as_class=False):
"""Returns coordinate conversions with their grid mappings.

.. versionadded:: GMVER

:Parameters:

as_class: `bool`, optional
If `True`, return the grid mapping as the equivalent
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
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"}
>>> f.get_grid_mappings(as_class=True)
{'coordinatereference1': cf.gridmappings.RotatedLatitudeLongitude}
>>> g.get_grid_mappings()
{}

"""
gms = {}
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
)
if gm:
if as_class:
try:
gm_class = GM(cref)
gms[cref_name] = gm_class
except InvalidGridMapping:
pass # not a supported GM so don't add
else:
gms[cref_name] = gm
return gms

def identity(self, default="", strict=False, relaxed=False, nc_only=False):
"""Return the canonical identity.

Expand Down
37 changes: 37 additions & 0 deletions cf/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -15667,3 +15667,40 @@ def unlimited(self, *args):
version="3.0.0",
removed_at="4.0.0",
) # pragma: no cover

def get_grid_mappings(self, as_class=False):
"""Returns coordinate conversions with their grid mappings.

.. versionadded:: GMVER

:Parameters:

as_class: `bool`, optional
If `True`, return the grid mapping as the equivalent
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
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"}
>>> f.get_grid_mappings(as_class=True)
{'coordinatereference1': cf.gridmappings.RotatedLatitudeLongitude}
>>> g.get_grid_mappings()
{}

"""
return self.domain.get_grid_mappings(as_class=as_class)
40 changes: 40 additions & 0 deletions cf/gridmappings/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
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.

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 .abstract import *
from .gridmapping import (
GM,
InvalidGridMapping,
AlbersEqualArea,
AzimuthalEquidistant,
Geostationary,
LambertAzimuthalEqualArea,
LambertConformalConic,
LambertCylindricalEqualArea,
Mercator,
ObliqueMercator,
Orthographic,
PolarStereographic,
RotatedLatitudeLongitude,
LatitudeLongitude,
Sinusoidal,
Stereographic,
TransverseMercator,
VerticalPerspective,
)
25 changes: 25 additions & 0 deletions cf/gridmappings/abstract/__init__.py
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions cf/gridmappings/abstract/azimuthal.py
Original file line number Diff line number Diff line change
@@ -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
)
Loading