Skip to content

Commit a6d4d85

Browse files
authored
MDT and decay (#56)
* use MDT (with EGM2008) for FES shifts * decay_pixels = 100 is default * ruff format
1 parent b7baa8a commit a6d4d85

5 files changed

Lines changed: 45 additions & 33 deletions

File tree

src/transformez/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def transform_raster(
190190
input_raster: str,
191191
datum_in: str,
192192
datum_out: str,
193-
decay_pixels: int = 0,
193+
decay_pixels: int = 100,
194194
output_raster: Optional[str] = None,
195195
cache_dir: Optional[str] = None,
196196
z_unit_in: Optional[str] = "auto",

src/transformez/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def transform_run(
174174
)
175175
@click.option("--out", "-o", help="Output filename (default: auto-named).")
176176
@click.option(
177-
"--decay-pixels", type=int, default=0, help="Pixels to decay tidal shifts inland."
177+
"--decay-pixels", type=int, default=100, help="Pixels to decay tidal shifts inland."
178178
)
179179
@click.option(
180180
"--use-stations",
@@ -252,7 +252,7 @@ def transform_grid(
252252
)
253253
@click.option("--out", "-o", help="Output filename (default: auto-named).")
254254
@click.option(
255-
"--decay-pixels", type=int, default=0, help="Pixels to decay tidal shifts inland."
255+
"--decay-pixels", type=int, default=100, help="Pixels to decay tidal shifts inland."
256256
)
257257
@click.option(
258258
"--use-stations",

src/transformez/definitions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,11 +402,13 @@ def get_unit_factor(cls, unit_str):
402402
"name": "EGM2008 height",
403403
"vdatum_id": "egm2008:m:height",
404404
"default_geoid": "egm2008",
405+
"ellipsoid": 4979,
405406
},
406407
5773: {
407408
"name": "EGM96 height",
408409
"vdatum_id": "egm96:m:height",
409410
"default_geoid": "egm96",
411+
"ellipsoid": 4979,
410412
},
411413
# # Ellipsoidal (Hubs) - No Geoid needed
412414
# 6319: {'name': 'NAD83(2011)', 'vdatum_id': 'nad83_2011:m:height'},

src/transformez/grid_engine.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,20 +251,25 @@ def coastal_aware_composite(
251251
return final_grid
252252

253253
@staticmethod
254-
def fill_nans(data, decay_pixels=0, buffer_pixels=10, land_mask=None):
254+
def fill_nans(data, decay_pixels=100, buffer_pixels=10, land_mask=None):
255255
"""Fills NaNs by extrapolating nearest valid coastal values.
256256
Melted Voronoi ridges ensure C1 continuity deep inland.
257257
"""
258258

259-
mask = np.isnan(data)
259+
out_data = data.copy()
260+
261+
if land_mask is not None:
262+
out_data[~land_mask] = np.nan
263+
264+
mask = np.isnan(out_data)
260265
if not mask.any() or mask.all():
261-
return data
266+
return out_data
262267

263268
dist, indices = ndimage.distance_transform_edt(
264269
mask, return_distances=True, return_indices=True
265270
)
266271

267-
raw_extrapolation = data[tuple(indices)]
272+
raw_extrapolation = out_data[tuple(indices)]
268273
# Blur the "Voronoi Ridges" deep inland
269274
blurred_extrapolation = ndimage.gaussian_filter(raw_extrapolation, sigma=25)
270275
# Crossfade! Beach = Raw Data, Inland = Blurred Data
@@ -273,8 +278,6 @@ def fill_nans(data, decay_pixels=0, buffer_pixels=10, land_mask=None):
273278
blurred_extrapolation * blur_blend
274279
)
275280

276-
out_data = data.copy()
277-
278281
if decay_pixels and decay_pixels > 0:
279282
# --- Inland Decay ---
280283
effective_dist = np.clip(dist - buffer_pixels, 0, None)

src/transformez/transform.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -594,12 +594,13 @@ def _get_vdatum_chain(self, datum_name, geoid_name):
594594
def _get_global_chain(self, datum_name, model="fes2014"):
595595
"""Builds shift: Global Tidal -> WGS84 Native."""
596596

597-
total_shift = np.zeros((self.ny, self.nx))
597+
tidal_shift = np.zeros((self.ny, self.nx))
598+
mss_shift = np.zeros((self.ny, self.nx))
598599
desc = []
599600

600601
model_def = Datums.MODELS.get(model)
601602
if not model_def:
602-
return total_shift, "Error"
603+
return tidal_shift, "Error"
603604

604605
provider = model_def["provider"]
605606

@@ -608,26 +609,18 @@ def _get_global_chain(self, datum_name, model="fes2014"):
608609
grid_name = model_def["grids"].get("lat")
609610
if grid_name:
610611
grid = self._get_grid(provider, grid_name)
611-
# Sign Correction (LAT should be negative relative to MSS)
612612
if np.nanmean(grid) > 0:
613613
grid *= -1.0
614-
615-
total_shift += grid
614+
tidal_shift += grid
616615
desc.append("LAT->MSS")
617616

618617
elif datum_name == "hat":
619618
lat_name = model_def["grids"].get("lat")
620619
if lat_name:
621-
logger.info("HAT requested. Computing FES deep-water symmetry...")
622620
grid = self._get_grid(provider, lat_name)
623-
624621
if np.nanmean(grid) > 0:
625622
grid *= -1.0
626-
627-
# Symmetry Math: HAT offset is the exact inverse of LAT offset
628-
hat_grid = grid * -1.0
629-
630-
total_shift += hat_grid
623+
tidal_shift += grid * -1.0
631624
desc.append("HAT->MSS(Symmetry)")
632625

633626
# MSS -> WGS84
@@ -640,23 +633,36 @@ def _get_global_chain(self, datum_name, model="fes2014"):
640633
else:
641634
desc.append("MSS->Ellipsoid")
642635

643-
total_shift += mss_grid
644-
645-
if np.isnan(total_shift).any():
646-
coast_shapefiles = self._fetch_coastline_shapefiles()
647-
land_mask = None
648-
if coast_shapefiles:
649-
land_mask = GridEngine.create_land_mask(
650-
self.region, self.nx, self.ny, coast_shapefiles
651-
)
636+
# --- MDT ---
637+
# Isolate the Mean Dynamic Topography (MDT) by subtracting EGM2008
638+
egm_grid, _ = self._fetch_geoid_with_fallback("egm2008")
639+
mdt_grid = mss_grid - egm_grid
640+
desc.append("MDT Decay -> EGM2008")
641+
642+
coast_shapefiles = self._fetch_coastline_shapefiles()
643+
land_mask = None
644+
if coast_shapefiles:
645+
land_mask = GridEngine.create_land_mask(
646+
self.region, self.nx, self.ny, coast_shapefiles
647+
)
652648

653-
total_shift = GridEngine.fill_nans(
654-
total_shift,
649+
if mss_name:
650+
mdt_decayed = GridEngine.fill_nans(
651+
mdt_grid,
655652
decay_pixels=self.decay_pixels,
656653
buffer_pixels=10,
657654
land_mask=land_mask,
658655
)
656+
mss_shift = mdt_decayed + egm_grid
659657

658+
tidal_shift = GridEngine.fill_nans(
659+
tidal_shift,
660+
decay_pixels=self.decay_pixels,
661+
buffer_pixels=10,
662+
land_mask=land_mask,
663+
)
664+
665+
total_shift = tidal_shift + mss_shift
660666
if not desc:
661667
return total_shift, "Global Chain (Empty)"
662668

@@ -764,7 +770,8 @@ def _step_from_hub(self, epsg, ref_type, geoid=None, epoch=None):
764770
s, d = self._get_global_chain(datum_name)
765771
if s is not None:
766772
total_out -= s
767-
desc_parts.append(f"Native -> Global({datum_name})")
773+
# desc_parts.append(f"Native -> Global({datum_name})")
774+
desc_parts.append(d)
768775

769776
elif ref_type == "cdn":
770777
target_geoid = geoid if geoid else "g2018"

0 commit comments

Comments
 (0)