@@ -100,6 +100,7 @@ def _get_pmm_transform_step(
100100 target_itrf : str ,
101101 target_epoch : str | None ,
102102 plate : str | None ,
103+ pmm_use_orb : bool ,
103104):
104105 """Perform coordinate propagation to the target epoch using a proj-provided plate
105106 motion model (PMM).
@@ -116,21 +117,100 @@ def _get_pmm_transform_step(
116117 (`t_observed`) is 1993.0 (1993-01-01T00:00:00) and the t_epoch is 2011.0
117118 (2011-01-01T00:00:00), then the delta is 1993 - 2011: -18. We need to invert
118119 the step so that the point is propagated forward in time, from 1993 to 2011.
120+
121+ If `pmm_use_orb` is `True`, then this function will use Origin Rate Bias (ORB)
122+ parameters for the selected PMM, if available (e.g., ITRF2020 and ITRF2008
123+ init files have ORB parameters defined; ITRF2014 has no ORB parameter sets).
119124 """
120125 plate_model_step = ""
121- if target_epoch :
122- if not plate :
123- plate = plate_name (Point (data .longitude .mean (), data .latitude .mean ()))
126+ if not target_epoch :
127+ return plate_model_step
128+
129+ if not plate :
130+ plate = plate_name (Point (data .longitude .mean (), data .latitude .mean ()))
131+
132+ if pmm_use_orb :
133+ plate_model_name = f"{ plate } _T"
124134 plate_model_step = (
125- f"+step +inv +init={ target_itrf } :{ plate } +t_epoch={ target_epoch } "
135+ f"+step +inv +init={ target_itrf } :{ plate_model_name } +t_epoch={ target_epoch } "
126136 )
127- if not _check_valid_proj_step (plate_model_step ):
128- err_msg = f"Failed to find pre-defined plate-model parameters for { target_itrf } :{ plate } "
129- raise RuntimeError (err_msg )
137+
138+ # If the step is valid, then return the step as-is. Otherwise, fallback
139+ # on the non-ORB step.
140+ if _check_valid_proj_step (plate_model_step ):
141+ return plate_model_step
142+
143+ plate_model_name = f"{ plate } "
144+ plate_model_step = (
145+ f"+step +inv +init={ target_itrf } :{ plate_model_name } +t_epoch={ target_epoch } "
146+ )
147+
148+ if not _check_valid_proj_step (plate_model_step ):
149+ err_msg = f"Failed to find pre-defined plate-model parameters for { target_itrf } :{ plate_model_name } "
150+ raise RuntimeError (err_msg )
130151
131152 return plate_model_step
132153
133154
155+ def _run_itrf_pipeline (
156+ data : IceflowDataFrame ,
157+ itrf_transformation_step : str ,
158+ plate_model_step : str ,
159+ target_itrf : str ,
160+ ):
161+ pipeline = (
162+ # This initializes the pipeline and declares the use of the WGS84
163+ # ellipsoid for all of the following steps. See
164+ # https://proj.org/en/9.5/operations/pipeline.html.
165+ f"+proj=pipeline +ellps=WGS84 "
166+ # Performs unit conversion from lon/lat degrees to radians.
167+ # TODO: This step appears to be unnecessary. Removing it does not appear to
168+ # affect the output. The following steps require that the
169+ # coordinates be geodedic, which could be radians or degrees.
170+ f"+step +proj=unitconvert +xy_in=deg +xy_out=rad "
171+ # This step explicitly sets the projection as lat/lon. It won't
172+ # change the coordinates, but they will be identified as geodetic,
173+ # which is necessary for the next steps.
174+ f"+step +proj=latlon "
175+ # Convert from lat/lon/elev geodetic coordinates to cartesian
176+ # coordinates, which are required for the following steps.
177+ # See: https://proj.org/en/9.5/operations/conversions/cart.html
178+ f"+step +proj=cart "
179+ # ITRF transformation. See above for definition.
180+ f"{ itrf_transformation_step } "
181+ # See above for definition.
182+ f"{ plate_model_step } "
183+ # Convert back from cartesian to lat/lon coordinates
184+ f"+step +inv +proj=cart "
185+ # Convert lon/lat from radians back to degrees.
186+ # TODO: remove this if the initial conversion to radians above is not needed
187+ f"+step +proj=unitconvert +xy_in=rad +xy_out=deg"
188+ )
189+
190+ transformer = pyproj .Transformer .from_pipeline (pipeline )
191+
192+ decimalyears = (
193+ data .reset_index ().utc_datetime .apply (_datetime_to_decimal_year ).to_numpy ()
194+ )
195+ # TODO: Should we create a new decimalyears when doing an epoch
196+ # propagation since PROJ doesn't do this?
197+
198+ lons , lats , elevs , _ = transformer .transform (
199+ data .longitude ,
200+ data .latitude ,
201+ data .elevation ,
202+ decimalyears ,
203+ )
204+
205+ transformed_data = data .copy ()
206+ transformed_data ["latitude" ] = lats
207+ transformed_data ["longitude" ] = lons
208+ transformed_data ["elevation" ] = elevs
209+ transformed_data ["ITRF" ] = target_itrf
210+
211+ return transformed_data
212+
213+
134214@pa .check_types ()
135215def transform_itrf (
136216 data : IceflowDataFrame ,
@@ -140,6 +220,10 @@ def transform_itrf(
140220 # is given but the plate name is not, each source ITRF is grouped together
141221 # and the mean of that chunk is used to determine the plate name.
142222 plate : str | None = None ,
223+ # Optional toggle for using ORB parameters, when available.
224+ # Defaults to `True` because this is recommended per the PMM authors. See
225+ # https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2023GL106373.
226+ pmm_use_orb : bool = True ,
143227) -> IceflowDataFrame :
144228 """Transform the data's latitude/longitude/elevation variables from the
145229 source ITRF to the target ITRF.
@@ -154,6 +238,13 @@ def transform_itrf(
154238 defined for the target_epoch, so it is likely to be most accurate for points
155239 observed near the ITRF's defined epoch.
156240
241+ If `pmm_use_orb` is `True`, then this function will use Origin Rate Bias (ORB)
242+ parameters for the selected PMM, if available (e.g., ITRF2020 and ITRF2008
243+ init files have ORB parameters defined; ITRF2014 has no ORB parameter sets),
244+ for the horizontal component of each point (`longitude` and `latitude`). The
245+ vertical component (`elevation`) will be transformed without ORB parameters
246+ applied.
247+
157248 All ITRF and PMM transformations are dependent on the user's `proj`
158249 installation's ITRF init files (see
159250 https://proj.org/en/9.3/resource_files.html#init-files). For example,
@@ -171,6 +262,7 @@ def transform_itrf(
171262 # TLAT/TLON/TZ are only available in ILVIS2v2 data:
172263 sel_ilvis2v2 = data.dataset == "ILVIS2v2"
173264 data.loc[sel_ilvis2v2, ["latitude", "longitude", "elevation"]] = data.loc[sel_ilvis2v2, ["TLAT", "TLON", "ZT"]]
265+
174266 ```
175267 """
176268 if not check_itrf (target_itrf ):
@@ -188,65 +280,50 @@ def transform_itrf(
188280 transformed_chunks .append (chunk )
189281 continue
190282
283+ itrf_transformation_step = _itrf_transformation_step (source_itrf , target_itrf )
284+
191285 chunk = cast (IceflowDataFrame , chunk )
286+
192287 plate_model_step = _get_pmm_transform_step (
193288 chunk ,
194289 target_itrf ,
195290 target_epoch ,
196291 plate ,
292+ pmm_use_orb = False ,
197293 )
198294
199- itrf_transformation_step = _itrf_transformation_step (source_itrf , target_itrf )
200-
201- pipeline = (
202- # This initializes the pipeline and declares the use of the WGS84
203- # ellipsoid for all of the following steps. See
204- # https://proj.org/en/9.5/operations/pipeline.html.
205- f"+proj=pipeline +ellps=WGS84 "
206- # Performs unit conversion from lon/lat degrees to radians.
207- # TODO: This step appears to be unnecessary. Removing it does not appear to
208- # affect the output. The following steps require that the
209- # coordinates be geodedic, which could be radians or degrees.
210- f"+step +proj=unitconvert +xy_in=deg +xy_out=rad "
211- # This step explicitly sets the projection as lat/lon. It won't
212- # change the coordinates, but they will be identified as geodetic,
213- # which is necessary for the next steps.
214- f"+step +proj=latlon "
215- # Convert from lat/lon/elev geodetic coordinates to cartesian
216- # coordinates, which are required for the following steps.
217- # See: https://proj.org/en/9.5/operations/conversions/cart.html
218- f"+step +proj=cart "
219- # ITRF transformation. See above for definition.
220- f"{ itrf_transformation_step } "
221- # See above for definition.
222- f"{ plate_model_step } "
223- # Convert back from cartesian to lat/lon coordinates
224- f"+step +inv +proj=cart "
225- # Convert lon/lat from radians back to degrees.
226- # TODO: remove this if the initial conversion to radians above is not needed
227- f"+step +proj=unitconvert +xy_in=rad +xy_out=deg"
295+ transformed_chunk = _run_itrf_pipeline (
296+ chunk ,
297+ itrf_transformation_step ,
298+ plate_model_step ,
299+ target_itrf ,
228300 )
229301
230- transformer = pyproj .Transformer .from_pipeline (pipeline )
231-
232- decimalyears = (
233- chunk .reset_index ().utc_datetime .apply (_datetime_to_decimal_year ).to_numpy ()
234- )
235- # TODO: Should we create a new decimalyears when doing an epoch
236- # propagation since PROJ doesn't do this?
237-
238- lons , lats , elevs , _ = transformer .transform (
239- chunk .longitude ,
240- chunk .latitude ,
241- chunk .elevation ,
242- decimalyears ,
243- )
302+ if pmm_use_orb :
303+ plate_model_step_with_orb = _get_pmm_transform_step (
304+ chunk ,
305+ target_itrf ,
306+ target_epoch ,
307+ plate ,
308+ pmm_use_orb = True ,
309+ )
310+
311+ # The ORB params are available if the resulting step is different.
312+ if plate_model_step_with_orb != plate_model_step :
313+ orb_transformed_chunk = _run_itrf_pipeline (
314+ chunk ,
315+ itrf_transformation_step ,
316+ plate_model_step_with_orb ,
317+ target_itrf ,
318+ )
319+ # Transform only the lat/lon elements, per
320+ # Z. Altamimi et. al. 2023 (https://doi.org/10.1029/2023GL106373):
321+ # > it is recommended to ignore the artifactual vertical component of the
322+ # > predicted velocities resulting from the addition of the ORB, and to only
323+ # > consider the horizontal components of the predicted velocities.
324+ transformed_chunk ["latitude" ] = orb_transformed_chunk .latitude
325+ transformed_chunk ["longitude" ] = orb_transformed_chunk .longitude
244326
245- transformed_chunk = chunk .copy ()
246- transformed_chunk ["latitude" ] = lats
247- transformed_chunk ["longitude" ] = lons
248- transformed_chunk ["elevation" ] = elevs
249- transformed_chunk ["ITRF" ] = target_itrf
250327 transformed_chunks .append (transformed_chunk )
251328
252329 transformed_df = pd .concat (transformed_chunks )
0 commit comments