Input and output formats

Points go in as plain Python or numpy values. There is no coordinate type to build first. Results come back as Coordinates, a tuple of tuples that can be exported to a list, a numpy array or a pandas DataFrame.

Values are always xy order: longitude before latitude, easting before northing, then height, wherever the CRS has such axes to order; a CRS without an east/north pair keeps its declared order (Axis order). Units are the CRS’s own axis units.

Ways to pass points

import numpy as np

from geodetic_engine.geodesy import Transformation

tfm = Transformation("EPSG:4230", "EPSG:4326", operation="EPSG:1612")

# One point, flat
print(tfm.transform((2.5, 63.5)).coordinates)

# A list of points
print(tfm.transform([(2.5, 63.5), (3.0, 64.0)]).coordinates)

# A 2D numpy array of shape (n_points, n_values)
print(tfm.transform(np.array([[2.5, 63.5], [3.0, 64.0]])).coordinates)

# Separate per-axis sequences, as pyproj's Transformer.transform takes them
print(tfm.transform([2.5, 3.0], [63.5, 64.0]).coordinates)
((2.4981894757094416, 63.49961374934551),)
((2.4981894757094416, 63.49961374934551), (2.9981763918698823, 63.99964022048465))
((2.4981894757094416, 63.49961374934551), (2.9981763918698823, 63.99964022048465))
((2.4981894757094416, 63.49961374934551), (2.9981763918698823, 63.99964022048465))

A lone scalar z is used for every point, so one height can be given once:

tfm.transform([2.5, 3.0], [63.5, 64.0], 100.0).coordinates
((2.4981894919143337, 63.49961375156072, 100.0),
 (2.998176408030172, 63.9996402223208, 100.0))

From a pandas DataFrame

Pass the columns in xy order, as an array or as separate series:

import pandas as pd

wells = pd.DataFrame(
    {"well": ["A-1", "B-2"], "lon": [10.75, 5.32], "lat": [59.91, 60.39]}
)
utm = Transformation("EPSG:4258", "EPSG:25832")

result = utm.transform(wells["lon"], wells["lat"])
wells[["E", "N"]] = result.coordinates.to_numpy()
wells
well lon lat E N
0 A-1 10.75 59.91 597868.381065 6.642682e+06
1 B-2 5.32 60.39 297230.220207 6.700510e+06

How many values per point

Each point must have one value per axis of the source CRS. It may have one more: a height given with a 2D horizontal CRS, which is carried through unchanged:

utm.transform([(10.75, 59.91, 123.4)]).coordinates
((597868.3810645777, 6642681.510038425, 123.4),)

Anything else is a ValueError naming the expected count:

utm.transform([(10.75,)])
ValueError: 1 values were given per point but CoordinateReferenceSystem('EPSG:4258') declares 2 axes; [2, 3] values are accepted (the extra being a height PROJ carries through unchanged)

Output always has one value per axis of the target CRS, plus a carried height if there was one. A vertical target gives one value per point.

Working with Coordinates

result.coordinates behaves like a tuple of tuples: index it, iterate over it, compare it. Three methods export it:

result = utm.transform([(10.75, 59.91), (5.32, 60.39)])
coords = result.coordinates

print(coords[0])           # first point
print(len(coords))         # number of points
print(coords.to_list())    # list of lists
print(coords.to_numpy())   # float64 array, shape (n_points, n_values)
coords.to_dataframe()      # columns named after the target CRS's axes
(597868.3810645777, 6642681.510038425)
2
[[597868.3810645777, 6642681.510038425], [297230.2202071025, 6700510.175130839]]
[[ 597868.38106458 6642681.51003843]
 [ 297230.2202071  6700510.17513084]]
E N
0 597868.381065 6.642682e+06
1 297230.220207 6.700510e+06

to_dataframe() names its columns with the target CRS’s value-order axis abbreviations, so a geographic target gives Lon and Lat, not x and y.

Round trip

Transform, then transform back with the same operation and swapped CRSs. The round trip for a Helmert reproduces the input to well under a millimetre:

forward = Transformation("EPSG:4230", "EPSG:4326", operation="EPSG:1612")
inverse = Transformation("EPSG:4326", "EPSG:4230", operation="EPSG:1612")

start = np.array([[2.5, 63.5, 100.0]])
there = forward.transform(start).coordinates.to_numpy()
back = inverse.transform(there).coordinates.to_numpy()
np.abs(back - start).max()
np.float64(1.6060930363437365e-09)