Applying Coordinate Transformations to Facilitate Data Comparison¶
Applying ITRF and Plate Movement Models to Lidar Data from Pre-IceBridge, ICESat/GLAS, IceBridge, and ICESat-2 Data Sets¶
Comparing measurements across altimetry data sets where latitude, longitude, and surface heights have been collected over long periods of time – a span of 25+ year in the case of ICESat/GLAS, IceBridge and ICESat-2 – requires us to account for evolving coordinate reference systems and tectonic plate movement. To demonstrate why plate movement matters over time, let’s look at the Australian plate. It is moving at a rate of around 7 cm / year, so over 25 years the continent will have moved nearly 1.8 m. All of Earth’s land masses are affected by plate movement to varying degrees, and over a multi-decade time series it’s important to recognize that the position recorded for a location in 1993 is going to be different from what would be recorded at that same location today. Plate movement and updates to coordinate reference systems over time must be accounted for when analyzing data over long time periods to avoid drawing spurious conclusions from the data.
Important Terms and Concepts¶
Coordinate system: The 2-dimensional (latitude, longitude) or 3-dimensional (latitude, longitude, height) system, and unit of measure used to define where a point is located in space.
Datum: A modeled version of the shape of the Earth which defines the origin used to place the coordinate system in space.
Coordinate Reference System (CRS): A CRS comprises a coordinate system and a datum.
Epoch (datum epoch): An epoch is used to identify the date at which a given reference frame was realized. They are also used to date when the coordinates for a particular observation (e.g., lat, lon, elevation) were obtained. Epochs are noted in decimal year format (YYYY.DD) where numbers to the right of the decimal are derived from the Julian day-in-year. The decimal year is equal to: year + day-in-year/365 (or 366 for leap years). For example, 1 January 2010 is written as 2010.00; 5 May 1991 is the 128th Julian day-in-year and written as 1991.35. To determine what month and day is represented by, say, 2018.5, multiply the numbers to the right of the decimal by the total number of days in 2018 (.5 * 365) to determine the Julian day-in-year is 183 (after rounding up to a whole number). Referencing a Julian date calendar (e.g., here ) we see that 2018.5 corresponds to 2 July 2018.
International Terrestrial Reference Frame (ITRF): A realization (i.e., a major adjustment) of the International Terrestrial Reference System (ITRS). Its origin is at the center of mass of the whole earth including the oceans and atmosphere. New ITRF solutions are produced every few years as the Earth changes and to employ the latest mathematical and surveying techniques to attempt to realize the ITRS as precisely as possible. Examples include ITRF88, ITRF93, ITRF2000, etc., with ITRF2014 being the latest realization of the ITRS. The ITRS is a formal description of the coordinate reference system, along with the mathematical and experimental procedures used to create reference frames and is maintained by the International Earth Rotation Service (IERS). The IERS issues parameters for transforming to and from older ITRF realizations. This makes it convenient to compare locations at any point on the Earth, but only for the same time, or epoch. Comparing coordinates in a dynamic frame from different epochs presents problems since the geographic features are being moved by plate tectonics, resulting in different coordinates for the same feature.
Example Data Set¶
For the remainder of the notebook, we will use a fictitious data set comprising latitude, longitude, and elevation coordinates. We will illustrate the use of PROJ and PyProj libraries to perform transformations between CRSs, and coordinate propagation (the shift in coordinates for a given geographic feature as a result of tectonic plate movement) between epochs in the same CRS using a Plate Motion Model. We construct a set of coordinates roughly on the Greenland ice sheet, with fictitious elevations. The particular values are used to illustrate the effects of coordinate transformation and propagation.
from __future__ import annotations
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from pyproj import Transformer
# We're going to illustrate an example of fictitious data from 1993 being transformed to be comparable to data from 2018.
epoch1 = 1993.5
itrf1 = "ITRF93"
epoch2 = 2018.5
itrf2 = "ITRF2014"
# Create lats and lons evenly spaced 1x1m grid (1 cm spacing) anchored to a point on the Greenland icesheet.
lat0 = 70.0
lon0 = -50.0
lat_deg_m = 1 / 111000.0 # (~degrees / m)
lon_deg_m = 1 / 34300.0 # (~degrees / m)
delta_lat = lat_deg_m * 0.01 # 1 cm
delta_lon = lon_deg_m * 0.01 # 1 cm
num_points = 11
lats = lat0 + np.arange(num_points) * delta_lat
lons = lon0 + np.arange(num_points) * delta_lon
lon_grid, lat_grid = np.meshgrid(lons, lats)
# Create elevations for illustration of coordinate transformation and propagation effects. The resulting "surface" will just be plane at constant elevation.
elev_grid = np.ones(lon_grid.shape)
# We set the observation epoch of the points of our sample data set.
epoch_grid = epoch1 * np.ones(lon_grid.shape)
Here we show a 10 x 10 grid spaced at 1 cm intervals, and set the elevations of each point at 1 m. The absolute latitude longitude values are at 70 N, 50 W, a spot “on” the Greenland ice sheet. We use this grid of points as our fictitious data set that we are going to transform using two methods. In the examples that follow, we’re going to assume this data was collected in 1993 and we’d like to compare it to data collected in 2018. The fictitious data set which we will transform will be in the ITRF93 reference frame, and assume the 2018 data we’d like to compare it to is in the ITRF2014 frame.
Note the scale difference on the z-axis. All axes units are meters.
fig = plt.figure(figsize=(16, 10))
ax = fig.add_subplot(111, projection="3d")
ax.view_init(40, 230)
ax.set_xlim(-0.01, 0.11)
ax.set_ylim(-0.01, 0.11)
ax.plot_wireframe(
(lon_grid - lon0) / lon_deg_m,
(lat_grid - lat0) / lat_deg_m,
elev_grid,
color="blue",
)
<mpl_toolkits.mplot3d.art3d.Line3DCollection at 0x787feefb32c0>

Coordinate Transformations between Reference Systems¶
As stated above, we’d like to transform the data collected in 1993 in the ITRF93 reference frame to allow comparison with points in the ITRF2014 frame. To do so, we create a PROJ pipeline that consists of a series of steps:
Declare that all the ellipsoids we’re dealing with are WGS84
Convert from degrees to radians
Convert from lat/lon/elevations to Cartesian coordinates
Convert from the ITRF93 frame to ITRF2014
Convert back to latitude / longitude coordinates
Convert from radians to degrees
def create_transformer(from_crs, to_crs, to_epoch=None):
plate_model_step = ""
if to_epoch:
plate_model_step = f"+step +inv +init={to_crs}:NOAM +t_epoch={to_epoch} "
pipeline = (
f"+proj=pipeline +ellps=WGS84 "
f"+step +proj=unitconvert +xy_in=deg +xy_out=rad "
f"+step +proj=latlon "
f"+step +proj=cart "
f"+step +inv +init={to_crs}:{from_crs} "
+ plate_model_step
+ "+step +proj=cart +inv "
"+step +proj=unitconvert +xy_in=rad +xy_out=deg"
)
return Transformer.from_pipeline(pipeline)
We now calculate the magnitude of the coordinate shifts from ITRF93 to ITRF2014, and then plot the original grid (blue) and the newly shifted grid (green).
t = create_transformer(itrf1, itrf2)
(lons_2014, lats_2014, elevs_2014, epochs_2014) = t.transform(
lon_grid.T.flatten(),
lat_grid.T.flatten(),
elev_grid,
epoch1 * np.ones(lon_grid.shape),
)
max_lon_delta = np.max(abs(lons_2014 - lon_grid.T.flatten()))
print(f"Longitude delta (m): {max_lon_delta / lon_deg_m:0.3f}")
max_lat_delta = np.max(abs(lats_2014 - lat_grid.T.flatten()))
print(f"Latitude delta (m): {max_lat_delta / lat_deg_m:0.3f}")
max_elev_delta = np.max(abs(elevs_2014 - elev_grid))
print(f"Elevation delta (m): {max_elev_delta:0.3f}")
lons_2014 = np.reshape(lons_2014, (num_points, num_points))
lats_2014 = np.reshape(lats_2014, (num_points, num_points))
Longitude delta (m): 0.005
Latitude delta (m): 0.028
Elevation delta (m): 0.005
fig = plt.figure(figsize=(16, 10))
ax = fig.add_subplot(111, projection="3d")
ax.view_init(40, 255)
ax.set_xlim(-0.01, 0.11)
ax.set_ylim(-0.01, 0.11)
ax.set_zlim(1, 1.01)
ax.plot_wireframe(
(lon_grid - lon0) / lon_deg_m,
(lat_grid - lat0) / lat_deg_m,
elev_grid,
color="blue",
)
ax.plot_wireframe(
(lons_2014 - lon0) / lon_deg_m,
(lats_2014 - lat0) / lat_deg_m,
elevs_2014,
color="green",
)
<mpl_toolkits.mplot3d.art3d.Line3DCollection at 0x787fec752c00>

Coordinate Propagation between Epochs within One Coordinate Reference System¶
Now that our example data set is in the same reference frame as the data with which we’d like to compare it, we’d like to account for the shift in coordinates for a given geographic feature as a result of tectonic plate movement. To do so, we can make use of the ITRF2014 Plate Motion Model (PMM) and apply both the ITRF93 => ITRF2014 coordinate transformation and the epoch 1993.5 => 2018.5 shift using the ITRFs PMM model parameters.
Here we show the original data set (blue), the data set with only the ITRF93 => ITRF2014 transformation applied (green), and the data set obtained by applying the PMM coordinate propagation from 1993.5 through 2018.5 in five-year increments (red). We see that the ITRF93 => ITRF2014 shift applied a relatively small vertical and horizontal shift, and the PMM results in a much larger shift roughly to the northwest. We see that the magnitude of this PMM shift is much more significant, and perhaps significant to the comparison of data between epochs.
def propagated_data(
from_crs, to_crs, obs_epoch, to_epoch, lon_grid, lat_grid, elev_grid
):
t = create_transformer(from_crs, to_crs, to_epoch)
(lons_new, lats_new, elevs_new, epochs_new) = t.transform(
lon_grid.T.flatten(),
lat_grid.T.flatten(),
elev_grid,
obs_epoch * np.ones(lon_grid.shape),
)
lons_new = np.reshape(lons_new, (num_points, num_points))
lats_new = np.reshape(lats_new, (num_points, num_points))
return (lons_new, lats_new, elevs_new, epochs_new)
fig = plt.figure(figsize=(16, 10))
ax = fig.add_subplot(111, projection="3d")
ax.view_init(40, 255)
ax.set_zlim(1, 1.01)
ax.plot_wireframe(
(lon_grid - lon0) / lon_deg_m,
(lat_grid - lat0) / lat_deg_m,
elev_grid,
color="blue",
)
count = 6
propd_data = {
y: propagated_data(itrf1, itrf2, epoch1, y, lon_grid, lat_grid, elev_grid)
for y in np.linspace(epoch1, epoch2, count)
}
print(propd_data.keys())
for _y, (lons, lats, elevs, _) in propd_data.items():
ax.plot_wireframe(
(lons - lon0) / lon_deg_m,
(lats - lat0) / lat_deg_m,
elevs,
color=mpl.colors.hsv_to_rgb([1, 0.5, 0.8]),
)
ax.plot_wireframe(
(lons_2014 - lon0) / lon_deg_m,
(lats_2014 - lat0) / lat_deg_m,
elevs_2014,
color="green",
)
dict_keys([np.float64(1993.5), np.float64(1998.5), np.float64(2003.5), np.float64(2008.5), np.float64(2013.5), np.float64(2018.5)])
<mpl_toolkits.mplot3d.art3d.Line3DCollection at 0x787feefb3a70>

max_lon_delta = np.max(abs(propd_data[2018.5][0] - propd_data[1993.5][0]))
print(f"Longitude delta (m): {max_lon_delta / lon_deg_m:0.2f}")
max_lat_delta = np.max(abs(propd_data[2018.5][1] - propd_data[1993.5][1]))
print(f"Latitude delta (m): {max_lat_delta / lat_deg_m:0.2f}")
max_elev_delta = np.max(abs(propd_data[2018.5][2] - propd_data[1993.5][2]))
print(f"Elevation delta (m): {max_elev_delta:0.4f}")
Longitude delta (m): 0.37
Latitude delta (m): 0.33
Elevation delta (m): 0.0007
References¶
Overview of the Reference Frame and Plate Shift Problem¶
The many paths to a common ground: A comparison of transformations between GDA94 and ITRF
Misaligned maps? High-accuracy data must become time-dependent
Re: Dynamic/static WGS84 datum problem with web-mapping and map misalignment
From static to dynamic datums: 150 years of geodetic datums in New Zealand
FIG/IAG/UN-GGIM-AP/UN-ICG/NZISTechnical Seminar Reference Frame in Practice