Skip to content

Commit 87bf5ef

Browse files
committed
WIP support using ORB params when available
1 parent fc1f700 commit 87bf5ef

File tree

1 file changed

+132
-55
lines changed

1 file changed

+132
-55
lines changed

src/nsidc/iceflow/itrf/converter.py

Lines changed: 132 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -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()
135215
def 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

Comments
 (0)