66
77import pandas as pd
88import pandera as pa
9- from pyproj import Transformer
9+ import pyproj
1010from shapely .geometry .point import Point
1111
1212from 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+
4157def _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 ()
0 commit comments