Coordinate reference systems

CoordinateReferenceSystem wraps a pyproj.crs.CRS and reports what EPSG declares about it: axis names, abbreviations, directions and units, in the declared order. You rarely need to build one, because transform() and Transformation accept the same inputs. Use it to check what a CRS expects before you transform into or out of it.

Resolving a CRS

Anything pyproj accepts works, plus OSDU persistableReference payloads:

from pyproj import CRS

from geodetic_engine.geodesy import CoordinateReferenceSystem

for value in (
    "EPSG:4326",                                  # authority code
    4326,                                         # bare integer: EPSG assumed
    "urn:ogc:def:crs:EPSG::25832",                # OGC URN
    CRS.from_epsg(5941),                          # a pyproj CRS
    "+proj=utm +zone=32 +ellps=GRS80 +units=m",   # PROJ string
):
    crs = CoordinateReferenceSystem.from_user_input(value)
    print(f"{crs.authority_code or '-':12} {crs.name}")
EPSG:4326    WGS 84
EPSG:4326    WGS 84
EPSG:25832   ETRS89 / UTM zone 32N
EPSG:5941    NN2000:2018 height
EPSG:25832   unknown

A string that is a persistableReference is recognised automatically. Use from_persistable_reference() to require one, so any other input is refused. See Persistable references (OSDU).

An unresolvable input raises UnresolvableCRSError, not a raw pyproj error:

CoordinateReferenceSystem.from_user_input("EPSG:999999")
UnresolvableCRSError: could not resolve 'EPSG:999999' as a CRS: Invalid projection: EPSG:999999: (Internal Proj Error: proj_create: crs not found: EPSG:999999)

Declared axes versus value order

EPSG declares EPSG:4326 latitude-first. This package passes values longitude-first. The CRS reports both:

wgs84 = CoordinateReferenceSystem.from_user_input("EPSG:4326")

print("declared  :", wgs84.axis_abbreviations)        # EPSG's order
print("values    :", wgs84.value_axis_abbreviations)  # the order you pass
print("mapping   :", wgs84.value_axis_order)          # value i is declared axis mapping[i]
print("units     :", wgs84.axis_units)
print("dimension :", wgs84.dimension)
declared  : ('Lat', 'Lon')
values    : ('Lon', 'Lat')
mapping   : (1, 0)
units     : ('degree', 'degree')
dimension : 2

value_axis_order gives, for each value you pass, the index of the declared axis it is. (1, 0) means the first value is the second declared axis (longitude).

Each axis has full detail in axes:

for axis in wgs84.axes:
    print(axis)
AxisSpec(name='Geodetic latitude', abbrev='Lat', direction='north', unit_name='degree', unit_code='9122', unit_conversion_factor=0.017453292519943295)
AxisSpec(name='Geodetic longitude', abbrev='Lon', direction='east', unit_name='degree', unit_code='9122', unit_conversion_factor=0.017453292519943295)

Projected CRSs are usually declared easting-first, so declared order and value order agree:

utm = CoordinateReferenceSystem.from_user_input("EPSG:25832")
utm.axis_abbreviations, utm.value_axis_abbreviations, utm.axis_units
(('E', 'N'), ('E', 'N'), ('metre', 'metre'))

Some are not. EPSG:2044 (Hanoi 1972 / Gauss-Kruger zone 18) is declared northing-first, and you still pass easting first:

gk = CoordinateReferenceSystem.from_user_input("EPSG:2044")
gk.axis_abbreviations, gk.value_axis_abbreviations
(('X', 'Y'), ('Y', 'X'))

Units

Units come from the CRS. This package does not convert them. Values go in and come out in the CRS’s own axis units. EPSG:4807 (NTF (Paris)) is in grads, with the pole at 100:

ntf_paris = CoordinateReferenceSystem.from_user_input("EPSG:4807")
ntf_paris.axis_units
('grad', 'grad')

A projected CRS in US survey feet reports that too:

texas = CoordinateReferenceSystem.from_user_input("EPSG:2278")  # NAD83 / Texas South Central (ftUS)
texas.name, texas.axis_units
('NAD83 / Texas South Central (ftUS)', ('US survey foot', 'US survey foot'))

Vertical, compound and 3D CRSs

dimension is the number of values each point has in this CRS:

for code in ("EPSG:5941", "EPSG:4979", "EPSG:6172", "EPSG:4978"):
    crs = CoordinateReferenceSystem.from_user_input(code)
    print(f"{code:10} dim={crs.dimension}  {crs.value_axis_abbreviations!s:18} {crs.name}")
EPSG:5941  dim=1  ('H',)             NN2000:2018 height
EPSG:4979  dim=3  ('Lon', 'Lat', 'h') WGS 84
EPSG:6172  dim=3  ('E', 'N', 'H')    ETRS89-NOR [EUREF89] / UTM zone 32N + NN54 height
EPSG:4978  dim=3  ('X', 'Y', 'Z')    WGS 84

EPSG:5941 is NN2000:2018 height (1D). EPSG:4979 is WGS 84 geographic 3D. EPSG:6172 is a compound CRS, ETRS89 / UTM 32N + NN54 height. EPSG:4978 is WGS 84 geocentric.

Dynamic CRSs

is_dynamic is true for a datum with a frame reference epoch, such as an ITRF realisation. A dynamic CRS does not by itself make a coordinate epoch mandatory. That depends on whether the operation reads one; see Time-dependent: a coordinate epoch.

for code in ("EPSG:4326", "EPSG:7789", "EPSG:4258"):
    crs = CoordinateReferenceSystem.from_user_input(code)
    print(f"{code:10} dynamic={crs.is_dynamic!s:5}  {crs.name}")
EPSG:4326  dynamic=False  WGS 84
EPSG:7789  dynamic=True   ITRF2014
EPSG:4258  dynamic=False  ETRS89

The underlying pyproj CRS

crs is the pyproj.crs.CRS, for anything this wrapper does not expose. definition is the text it was resolved from.

wgs84.crs.datum.name, wgs84.definition
('World Geodetic System 1984 ensemble', 'EPSG:4326')