Skip to content

Commit 2f98678

Browse files
Merge pull request #46 from nsidc/itrf-conversion-better-handling-source-target
More flexible ITRF conversion function
2 parents 5fc0b0a + 69bf34e commit 2f98678

File tree

5 files changed

+97
-21
lines changed

5 files changed

+97
-21
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# v1.0.0
2+
3+
- Update `transform_itrf` function to be more flexible. Both forward and reverse
4+
transformations between proj-supported ITRFs are now supported, which means
5+
that more ITRF-to-ITRF transformations are handled.
6+
17
# v0.3.0
28

39
- Use `pydantic` to create custom BoundingBox class for

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "nsidc-iceflow"
3-
version = "v0.3.0"
3+
version = "v1.0.0"
44
authors = [
55
{ name = "NSIDC", email = "[email protected]" },
66
]
@@ -190,7 +190,7 @@ messages_control.disable = [
190190
]
191191

192192
[tool.bumpversion]
193-
current_version = "0.3.0"
193+
current_version = "1.0.0"
194194
commit = false
195195
tag = false
196196

src/nsidc/iceflow/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
# TODO: add bumpversion config to control this version number, and the conda
3636
# recipe/meta.yaml.
37-
__version__ = "v0.3.0"
37+
__version__ = "v1.0.0"
3838

3939

4040
__all__ = [

src/nsidc/iceflow/itrf/converter.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import pandas as pd
88
import pandera as pa
9-
from pyproj import Transformer
9+
import pyproj
1010
from shapely.geometry.point import Point
1111

1212
from nsidc.iceflow.data.models import IceflowDataFrame
@@ -38,20 +38,61 @@ def sinceEpoch(date):
3838
return date.year + fraction
3939

4040

41+
def _check_valid_proj_step(proj_str) -> bool:
42+
"""Check if the source/target ITRF pair can be expanded.
43+
44+
Returns `True` if the combination is valid. Otherwise `False`.
45+
46+
The combination is valid only if there is an init file on the proj data path
47+
matching the `source_itrf` that has an entry matching the `target_itrf`. See
48+
https://proj.org/en/9.3/resource_files.html#init-files for more info.
49+
"""
50+
try:
51+
pyproj.Transformer.from_pipeline(proj_str)
52+
return True
53+
except pyproj.exceptions.ProjError:
54+
return False
55+
56+
4157
def _itrf_transformation_step(source_itrf: str, target_itrf: str) -> str:
42-
itrf_transformation_step = ""
43-
if source_itrf != target_itrf:
44-
# This performs a helmert transform (see
45-
# https://proj.org/en/9.4/operations/transformations/helmert.html). `+init=ITRF2014:ITRF2008`
46-
# looks up the ITRF2008 helmert transformation step in the ITRF2014
47-
# data file (see
48-
# https://proj.org/en/9.3/resource_files.html#init-files and e.g.,
49-
# https://github.com/OSGeo/PROJ/blob/master/data/ITRF2014). The
50-
# `+inv` reverses the transformation. So `+init=ITRF2014:ITRF2008`
51-
# performs a helmert transform from ITRF2008 to ITRF2014.
52-
itrf_transformation_step = f"+step +inv +init={target_itrf}:{source_itrf} "
58+
"""Get the ITRF transformation step for the given source/target ITRF.
5359
54-
return itrf_transformation_step
60+
The transformation step returned by this function performs a helmert
61+
transform (see
62+
https://proj.org/en/9.4/operations/transformations/helmert.html).
63+
64+
The parameters for the helmert transform come from proj init files (see
65+
https://proj.org/en/9.3/resource_files.html#init-files). For example,
66+
`+init=ITRF2014:ITRF2008` looks up the ITRF2008 helmert transformation step
67+
in the ITRF2014 data file (see
68+
https://github.com/OSGeo/PROJ/blob/master/data/ITRF2014).
69+
"""
70+
# The `+inv` reverses the transformation. So `+init=ITRF2014:ITRF2008`
71+
# performs a helmert transform from ITRF2008 to ITRF2014. This is the most
72+
# common case for `iceflow`, because we tend to be targeting pre-icesat2
73+
# data for transformation to ITRF2014 (icesat2), so try this first.
74+
inv_itrf_transformation_step = f"+step +inv +init={target_itrf}:{source_itrf}"
75+
if _check_valid_proj_step(inv_itrf_transformation_step):
76+
return inv_itrf_transformation_step
77+
78+
# Forward helmert transformation. `+init=ITRF2014:ITRF2008`
79+
# performs a helmert transform from ITRF2014 to ITRF2008.
80+
fwd_itrf_transformation_step = f"+step +init={source_itrf}:{target_itrf}"
81+
if _check_valid_proj_step(fwd_itrf_transformation_step):
82+
return fwd_itrf_transformation_step
83+
84+
# There may not be a pre-defined helmert transformation. The user may want
85+
# to craft their own transformation pipeline.
86+
err_msg = (
87+
f"Failed to find a pre-defined ITRF transformation between {source_itrf} and {target_itrf}."
88+
" ITRF transformation parameters are provided by proj's ITRF init files."
89+
" Consider upgrading proj to ensure the latest data is available and try again."
90+
" See https://proj.org/en/latest/resource_files.html#init-files for more information."
91+
f" If no pre-defined transformation is available for {source_itrf} -> {target_itrf},"
92+
" it may be possible to define your own transformation using parameters found at https://itrf.ign.fr/."
93+
" See https://proj.org/en/latest/operations/transformations/helmert.html for more information."
94+
)
95+
raise RuntimeError(err_msg)
5596

5697

5798
@pa.check_types()
@@ -91,8 +132,8 @@ def transform_itrf(
91132

92133
transformed_chunks = []
93134
for source_itrf, chunk in data.groupby(by="ITRF"):
94-
# If the source ITRF is the same as the target for this chunk, skip transformation.
95135
source_itrf = cast(str, source_itrf)
136+
# If the source ITRF is the same as the target for this chunk, skip transformation.
96137
if source_itrf == target_itrf and target_epoch is None:
97138
transformed_chunks.append(chunk)
98139
continue
@@ -120,8 +161,11 @@ def transform_itrf(
120161
# is 2011.0 (2011-01-01T00:00:00), then the delta is 1993 -
121162
# 2011: -18. We need to invert the step so that the point is
122163
# propagated forward in time, from 1993 to 2011.
123-
f"+step +inv +init={target_itrf}:{plate} +t_epoch={target_epoch} "
164+
f"+step +inv +init={target_itrf}:{plate} +t_epoch={target_epoch}"
124165
)
166+
if not _check_valid_proj_step(plate_model_step):
167+
err_msg = f"Failed to find pre-defined plate-model parameters for {target_itrf}:{plate}"
168+
raise RuntimeError(err_msg)
125169

126170
itrf_transformation_step = _itrf_transformation_step(source_itrf, target_itrf)
127171

@@ -144,17 +188,17 @@ def transform_itrf(
144188
# See: https://proj.org/en/9.5/operations/conversions/cart.html
145189
f"+step +proj=cart "
146190
# ITRF transformation. See above for definition.
147-
f"{itrf_transformation_step}"
191+
f"{itrf_transformation_step} "
148192
# See above for definition.
149-
f"{plate_model_step}"
193+
f"{plate_model_step} "
150194
# Convert back from cartesian to lat/lon coordinates
151195
f"+step +inv +proj=cart "
152196
# Convert lon/lat from radians back to degrees.
153197
# TODO: remove this if the initial conversion to radians above is not needed
154198
f"+step +proj=unitconvert +xy_in=rad +xy_out=deg"
155199
)
156200

157-
transformer = Transformer.from_pipeline(pipeline)
201+
transformer = pyproj.Transformer.from_pipeline(pipeline)
158202

159203
decimalyears = (
160204
chunk.reset_index().utc_datetime.apply(_datetime_to_decimal_year).to_numpy()

tests/unit/test_itrf_converter.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,32 @@ def test__itrf_transformation_step():
166166
assert np.round(actual_z, 4) == expected_z
167167

168168

169+
def test__itrf_transformation_step_failure():
170+
"""Test that the _itrf_transformation_step raises an error if there is no
171+
pre-defined transform."""
172+
173+
with pytest.raises(
174+
RuntimeError, match="Failed to find a pre-defined ITRF transformation"
175+
):
176+
_itrf_transformation_step("ITRF2022", "ITRF93")
177+
178+
179+
def test__itrf_transformation_step_fwd():
180+
"""Test that the forward transform is returned."""
181+
expected = "+step +init=ITRF2014:ITRF2008"
182+
actual_step = _itrf_transformation_step("ITRF2014", "ITRF2008")
183+
184+
assert expected == actual_step
185+
186+
187+
def test__itrf_transformation_step_inv():
188+
"""Test that the forward transform is returned."""
189+
expected = "+step +inv +init=ITRF2014:ITRF2008"
190+
actual_step = _itrf_transformation_step("ITRF2008", "ITRF2014")
191+
192+
assert expected == actual_step
193+
194+
169195
@pytest.mark.parametrize("timezone", ["America/Denver", "UTC"])
170196
def test__datetime_to_decimal_year(timezone, monkeypatch):
171197
monkeypatch.setenv("TZ", timezone)

0 commit comments

Comments
 (0)