transformez 0.5.2__tar.gz → 0.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {transformez-0.5.2 → transformez-0.5.3}/CITATION.cff +2 -2
- {transformez-0.5.2 → transformez-0.5.3}/PKG-INFO +2 -10
- {transformez-0.5.2 → transformez-0.5.3}/README.md +1 -9
- {transformez-0.5.2 → transformez-0.5.3}/pyproject.toml +2 -2
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/_version.py +1 -1
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/api.py +124 -0
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/cli.py +67 -4
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/definitions.py +7 -6
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/grid_engine.py +47 -1
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/htdp.py +57 -31
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/srs.py +7 -11
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/transform.py +59 -10
- {transformez-0.5.2 → transformez-0.5.3}/AUTHORS.md +0 -0
- {transformez-0.5.2 → transformez-0.5.3}/CHANGELOG.md +0 -0
- {transformez-0.5.2 → transformez-0.5.3}/CONTRIBUTING.md +0 -0
- {transformez-0.5.2 → transformez-0.5.3}/LICENSE +0 -0
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/__init__.py +0 -0
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/_spatial.py +0 -0
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/hooks.py +0 -0
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/modules.py +0 -0
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/utils.py +0 -0
- {transformez-0.5.2 → transformez-0.5.3}/src/transformez/vdatum.py +0 -0
- {transformez-0.5.2 → transformez-0.5.3}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: transformez
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: A standalone utility for vertical elevation datum transformations.
|
|
5
5
|
Project-URL: Homepage, https://github.com/continuous-dems/transformez
|
|
6
6
|
Project-URL: Issues, https://github.com/continuous-dems/transformez/issues
|
|
@@ -57,7 +57,7 @@ Description-Content-Type: text/markdown
|
|
|
57
57
|
<p align="center"><strong>Global vertical datum transformations, simplified.</strong></p>
|
|
58
58
|
|
|
59
59
|
<p align="center">
|
|
60
|
-
<a href="https://github.com/continuous-dems/transformez"><img src="https://img.shields.io/badge/version-0.5.
|
|
60
|
+
<a href="https://github.com/continuous-dems/transformez"><img src="https://img.shields.io/badge/version-0.5.3-blue.svg" alt="Version"></a>
|
|
61
61
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
|
|
62
62
|
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.12+-yellow.svg" alt="Python"></a>
|
|
63
63
|
<a href="https://badge.fury.io/py/transformez"><img src="https://badge.fury.io/py/transformez.svg" alt="PyPI version"></a>
|
|
@@ -71,14 +71,6 @@ Originally developed as the core transformation engine for the [CUDEM](https://g
|
|
|
71
71
|
|
|
72
72
|
---
|
|
73
73
|
|
|
74
|
-
## ❓ Why Transformez?
|
|
75
|
-
|
|
76
|
-
Vertical Datum transformations can cause a lot of confusion and headache when trying to process heterogeneous data from around the world into a single unified vertial reference.
|
|
77
|
-
|
|
78
|
-
**Transformez simplifies this process!**
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
74
|
## 📦 Installation
|
|
83
75
|
|
|
84
76
|
**Install Transformez**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<p align="center"><strong>Global vertical datum transformations, simplified.</strong></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<a href="https://github.com/continuous-dems/transformez"><img src="https://img.shields.io/badge/version-0.5.
|
|
10
|
+
<a href="https://github.com/continuous-dems/transformez"><img src="https://img.shields.io/badge/version-0.5.3-blue.svg" alt="Version"></a>
|
|
11
11
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
|
|
12
12
|
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.12+-yellow.svg" alt="Python"></a>
|
|
13
13
|
<a href="https://badge.fury.io/py/transformez"><img src="https://badge.fury.io/py/transformez.svg" alt="PyPI version"></a>
|
|
@@ -21,14 +21,6 @@ Originally developed as the core transformation engine for the [CUDEM](https://g
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
-
## ❓ Why Transformez?
|
|
25
|
-
|
|
26
|
-
Vertical Datum transformations can cause a lot of confusion and headache when trying to process heterogeneous data from around the world into a single unified vertial reference.
|
|
27
|
-
|
|
28
|
-
**Transformez simplifies this process!**
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
24
|
## 📦 Installation
|
|
33
25
|
|
|
34
26
|
**Install Transformez**
|
|
@@ -359,3 +359,127 @@ def transform_raster(
|
|
|
359
359
|
if success:
|
|
360
360
|
return output_raster
|
|
361
361
|
return None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def prefetch_region(
|
|
365
|
+
region: Union[List[float], str, Region],
|
|
366
|
+
datum_in: Optional[str] = None,
|
|
367
|
+
datum_out: Optional[str] = None,
|
|
368
|
+
fetch_all: bool = False,
|
|
369
|
+
cache_dir: Optional[str] = None,
|
|
370
|
+
verbose: bool = True,
|
|
371
|
+
) -> bool:
|
|
372
|
+
"""Pre-download transformation grids and reference datasets for offline field use.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
region: Bounds as [W, E, S, N], a 'loc:' string, or a Region object.
|
|
376
|
+
datum_in: Source datum string (e.g. 'mllw') to limit fetching to a specific chain.
|
|
377
|
+
datum_out: Target datum string (e.g. '5703') to limit fetching to a specific chain.
|
|
378
|
+
fetch_all: If True, fetches ALL available geoids, tidal surfaces, and coastlines for the region.
|
|
379
|
+
cache_dir: Directory where downloaded assets will be cached.
|
|
380
|
+
verbose: Enable detailed logging.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
bool: True if prefetching succeeded, False otherwise.
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
if isinstance(region, Region):
|
|
387
|
+
region_obj = region
|
|
388
|
+
else:
|
|
389
|
+
regions = parse_region(region)
|
|
390
|
+
if not regions:
|
|
391
|
+
logger.error(f"Could not parse region: {region}")
|
|
392
|
+
return False
|
|
393
|
+
region_obj = regions[0]
|
|
394
|
+
|
|
395
|
+
logger.info(f"Initiating offline prefetch for region: {region_obj}")
|
|
396
|
+
|
|
397
|
+
# Minimal dimensions (10x10) to avoid allocating memory for large arrays
|
|
398
|
+
vt_nx, vt_ny = 10, 10
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
if fetch_all or (not datum_in and not datum_out):
|
|
402
|
+
logger.info(
|
|
403
|
+
"Mode: FULL PREFETCH. Downloading all geoids, VDatum grids, and coastlines..."
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Instantiate base engine to leverage internal fetchers
|
|
407
|
+
vt = VerticalTransform(
|
|
408
|
+
region=region_obj,
|
|
409
|
+
nx=vt_nx,
|
|
410
|
+
ny=vt_ny,
|
|
411
|
+
epsg_in=4979, # Base WGS84
|
|
412
|
+
epsg_out=6319, # Base NAD83
|
|
413
|
+
cache_dir=cache_dir,
|
|
414
|
+
verbose=verbose,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Vector Coastline Tiles (GSHHG / CUSP)
|
|
418
|
+
logger.info(" -> [1/5] Fetching coastline vector tiles...")
|
|
419
|
+
vt._fetch_coastline_shapefiles()
|
|
420
|
+
|
|
421
|
+
# All Registered Geoids
|
|
422
|
+
logger.info(" -> [2/5] Fetching Geoid grids...")
|
|
423
|
+
for g_name, g_def in Datums.GEOIDS.items():
|
|
424
|
+
provider = g_def.get("provider", "proj")
|
|
425
|
+
logger.info(f" - Fetching Geoid: {g_name} ({provider})")
|
|
426
|
+
try:
|
|
427
|
+
vt.fetch_grid(provider, datatype=g_name, query=g_name)
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.warning(f" - Skipping '{g_name}': {e}")
|
|
430
|
+
|
|
431
|
+
# USA VDatum Tidal Grids
|
|
432
|
+
logger.info(" -> [3/5] Fetching VDatum regional grids...")
|
|
433
|
+
for s_key, s_def in Datums.SURFACES.items():
|
|
434
|
+
s_name = s_def.get("name", s_key)
|
|
435
|
+
if s_def.get("region") == "usa":
|
|
436
|
+
logger.info(f" - Fetching VDatum Surface: {s_name}")
|
|
437
|
+
try:
|
|
438
|
+
vt.fetch_grid("vdatum", datatype=s_name, query=s_name)
|
|
439
|
+
except Exception as e:
|
|
440
|
+
logger.warning(f" - Skipping VDatum '{s_name}': {e}")
|
|
441
|
+
|
|
442
|
+
# Topography of the Sea Surface (TSS)
|
|
443
|
+
logger.info(" -> [4/5] Fetching VDatum TSS grid...")
|
|
444
|
+
try:
|
|
445
|
+
vt.fetch_grid("vdatum", datatype="tss", query="tss")
|
|
446
|
+
except Exception as e:
|
|
447
|
+
logger.warning(f" - Skipping TSS: {e}")
|
|
448
|
+
|
|
449
|
+
# Global Satellite Models (FES / SEANOE)
|
|
450
|
+
logger.info(" -> [5/5] Fetching Global FES / MSS proxy grids...")
|
|
451
|
+
for proxy_name in ["lat", "msl", "mss"]:
|
|
452
|
+
logger.info(f" - Fetching Global Proxy: {proxy_name}")
|
|
453
|
+
try:
|
|
454
|
+
vt.fetch_grid("fes", datatype=proxy_name, query=proxy_name)
|
|
455
|
+
except Exception as e:
|
|
456
|
+
logger.warning(f" - Skipping Global '{proxy_name}': {e}")
|
|
457
|
+
|
|
458
|
+
else:
|
|
459
|
+
epsg_in, geoid_in = _parse_datum(datum_in) if datum_in else (4979, None)
|
|
460
|
+
epsg_out, geoid_out = _parse_datum(datum_out) if datum_out else (6319, None)
|
|
461
|
+
|
|
462
|
+
logger.info(
|
|
463
|
+
f"Mode: TARGETED PREFETCH for chain ({datum_in or 'WGS84'} ➔ {datum_out or 'NAD83'})..."
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
vt = VerticalTransform(
|
|
467
|
+
region=region_obj,
|
|
468
|
+
nx=vt_nx,
|
|
469
|
+
ny=vt_ny,
|
|
470
|
+
epsg_in=epsg_in or 4979,
|
|
471
|
+
epsg_out=epsg_out or 6319,
|
|
472
|
+
geoid_in=geoid_in,
|
|
473
|
+
geoid_out=geoid_out,
|
|
474
|
+
cache_dir=cache_dir,
|
|
475
|
+
verbose=verbose,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
vt._vertical_transform(vt.epsg_in, vt.epsg_out)
|
|
479
|
+
|
|
480
|
+
logger.info("Successfully populated offline cache!")
|
|
481
|
+
return True
|
|
482
|
+
|
|
483
|
+
except Exception as e:
|
|
484
|
+
logger.error(f"Prefetch failed: {e}")
|
|
485
|
+
return False
|
|
@@ -20,8 +20,8 @@ from fetchez.cli import setup_logging
|
|
|
20
20
|
from transformez import api
|
|
21
21
|
|
|
22
22
|
TRANSFORMEZ_COMMANDS = {
|
|
23
|
-
"Execution": ["
|
|
24
|
-
"Discovery": ["list"],
|
|
23
|
+
"Execution": ["grid", "raster"],
|
|
24
|
+
"Discovery": ["list", "prefetch"],
|
|
25
25
|
"External": ["htdp", "vdatum"],
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -401,7 +401,9 @@ def transform_list():
|
|
|
401
401
|
|
|
402
402
|
|
|
403
403
|
# --- HTDP CLI GROUP ---
|
|
404
|
-
@transformez_cli.group(
|
|
404
|
+
@transformez_cli.group(
|
|
405
|
+
cls=FetchezMainGroup, name="htdp", fetchez_commands=["install", "run"]
|
|
406
|
+
)
|
|
405
407
|
def htdp_group():
|
|
406
408
|
"""Manage and run NGS HTDP (Horizontal Time-Dependent Positioning)."""
|
|
407
409
|
|
|
@@ -422,9 +424,19 @@ def install_htdp(version):
|
|
|
422
424
|
install_htdp_binary(version=version)
|
|
423
425
|
|
|
424
426
|
|
|
427
|
+
@htdp_group.command("run", cls=FetchezMainCommand)
|
|
428
|
+
@click.option("--control", help="input control file, if omitted, run interactively")
|
|
429
|
+
def run_htpd(control):
|
|
430
|
+
"""Run HTDP from transformez"""
|
|
431
|
+
|
|
432
|
+
from transformez.htdp import HTDP
|
|
433
|
+
|
|
434
|
+
HTDP.run_cmd(control)
|
|
435
|
+
|
|
436
|
+
|
|
425
437
|
# --- VDATUM CLI GROUP ---
|
|
426
438
|
@transformez_cli.group(
|
|
427
|
-
cls=FetchezMainGroup, name="vdatum", fetchez_commands=["install"
|
|
439
|
+
cls=FetchezMainGroup, name="vdatum", fetchez_commands=["install"]
|
|
428
440
|
)
|
|
429
441
|
def vdatum_group():
|
|
430
442
|
"""Manage and run the NOAA VDatum Java engine."""
|
|
@@ -480,5 +492,56 @@ def vdatum_list():
|
|
|
480
492
|
click.echo(vd)
|
|
481
493
|
|
|
482
494
|
|
|
495
|
+
# --- PREFETCH CLI GROUP ---
|
|
496
|
+
@transformez_cli.command("prefetch", cls=FetchezMainCommand)
|
|
497
|
+
@click.option("-R", "--region", required=True, help="Bounding box or location string.")
|
|
498
|
+
@click.option(
|
|
499
|
+
"-I",
|
|
500
|
+
"--input-datum",
|
|
501
|
+
help="Source Datum (Optional: limits fetch to specific chain).",
|
|
502
|
+
)
|
|
503
|
+
@click.option(
|
|
504
|
+
"-O",
|
|
505
|
+
"--output-datum",
|
|
506
|
+
help="Target Datum (Optional: limits fetch to specific chain).",
|
|
507
|
+
)
|
|
508
|
+
@click.option(
|
|
509
|
+
"--all",
|
|
510
|
+
"fetch_all",
|
|
511
|
+
is_flag=True,
|
|
512
|
+
help="Fetch ALL available geoids, tidal models, and coastlines for this region.",
|
|
513
|
+
)
|
|
514
|
+
def transform_prefetch(region, input_datum, output_datum, fetch_all):
|
|
515
|
+
"""Pre-download transformation grids and reference data for offline field use.
|
|
516
|
+
|
|
517
|
+
Examples:\n
|
|
518
|
+
Prefetch a specific conversion : transformez prefetch -R loc:"Newport, OR" -I mllw -O 5703
|
|
519
|
+
Prefetch EVERYTHING for a region: transformez prefetch -R loc:"Miami, FL" --all
|
|
520
|
+
Prefetch the entire planet : transformez prefetch -R -180/180/-90/90 --all
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
click.secho(
|
|
524
|
+
f"Populating offline cache for region: {region}...", fg="cyan", bold=True
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
result = api.prefetch_region(
|
|
528
|
+
region=region,
|
|
529
|
+
datum_in=input_datum,
|
|
530
|
+
datum_out=output_datum,
|
|
531
|
+
fetch_all=fetch_all,
|
|
532
|
+
verbose=True,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
if result:
|
|
536
|
+
click.secho(
|
|
537
|
+
f"Successfully pre-cached all assets for {region}. Ready for offline use!",
|
|
538
|
+
fg="green",
|
|
539
|
+
bold=True,
|
|
540
|
+
)
|
|
541
|
+
else:
|
|
542
|
+
click.secho("Prefetch encountered errors. Check logs for details.", fg="red")
|
|
543
|
+
sys.exit(1)
|
|
544
|
+
|
|
545
|
+
|
|
483
546
|
if __name__ == "__main__":
|
|
484
547
|
transformez_cli()
|
|
@@ -12,6 +12,7 @@ and definitions.
|
|
|
12
12
|
:license: MIT, see LICENSE for more details.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
+
from typing import Any, Dict
|
|
15
16
|
import logging
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
@@ -42,7 +43,7 @@ class Datums:
|
|
|
42
43
|
# =========================================================================
|
|
43
44
|
# Vertical Datum References
|
|
44
45
|
# =========================================================================
|
|
45
|
-
SURFACES = {
|
|
46
|
+
SURFACES: Dict[int, Dict[str, Any]] = {
|
|
46
47
|
# --- Tidal Datums ---
|
|
47
48
|
1089: {"name": "mllw", "description": "Mean Lower Low Water", "region": "usa"},
|
|
48
49
|
5866: {"name": "mllw", "description": "Mean Lower Low Water", "region": "usa"},
|
|
@@ -104,7 +105,7 @@ class Datums:
|
|
|
104
105
|
9003: {"name": "mss", "description": "Mean Sea Surface", "region": "global"},
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
GLOBAL_ALIASES = {
|
|
108
|
+
GLOBAL_ALIASES: Dict[str, Any] = {
|
|
108
109
|
"mllw": "lat", # Mean Lower Low Water -> Lowest Astronomical Tide
|
|
109
110
|
"mlw": "lat", # Mean Low Water -> LAT (Conservative)
|
|
110
111
|
"mhhw": "hat", # Mean Higher High Water -> Highest Astronomical Tide
|
|
@@ -114,7 +115,7 @@ class Datums:
|
|
|
114
115
|
"dtl": "mss", # Diurnal Tide Level -> MSS
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
HTDP = {
|
|
118
|
+
HTDP: Dict[int, Dict[str, Any]] = {
|
|
118
119
|
4269: {
|
|
119
120
|
"name": "NAD_83(2011/CORS96/2007)",
|
|
120
121
|
"description": "(North American plate fixed)",
|
|
@@ -362,7 +363,7 @@ class Datums:
|
|
|
362
363
|
},
|
|
363
364
|
}
|
|
364
365
|
|
|
365
|
-
CDN = {
|
|
366
|
+
CDN: Dict[int, Dict[str, Any]] = {
|
|
366
367
|
# CONUS / Alaska / Hawaii / PR / VI
|
|
367
368
|
5703: {
|
|
368
369
|
"name": "NAVD88 height",
|
|
@@ -415,7 +416,7 @@ class Datums:
|
|
|
415
416
|
# 4979: {'name': 'WGS84', 'vdatum_id': 'wgs84:m:height'},
|
|
416
417
|
}
|
|
417
418
|
|
|
418
|
-
GEOIDS = {
|
|
419
|
+
GEOIDS: Dict[str, Dict[str, Any]] = {
|
|
419
420
|
# Standard PROJ-CDN Geoids (Default provider is 'proj')
|
|
420
421
|
"g2018": {"name": "geoid 2018", "uncertainty": 0.0127, "provider": "proj"},
|
|
421
422
|
"g2012b": {"name": "geoid 2012b", "uncertainty": 0.017, "provider": "proj"},
|
|
@@ -428,7 +429,7 @@ class Datums:
|
|
|
428
429
|
"CGG2013": {"name": "CGG2013", "uncertainty": 0.01, "provider": "proj"},
|
|
429
430
|
}
|
|
430
431
|
|
|
431
|
-
MODELS = {
|
|
432
|
+
MODELS: Dict[str, Dict[str, Any]] = {
|
|
432
433
|
"fes2014": {"provider": "seanoe", "grids": {"lat": "LAT", "mss": "MSL"}},
|
|
433
434
|
"dtu10": {"provider": "dtu", "grids": {"mss": "mss", "mdt": "mdt"}},
|
|
434
435
|
"egm2008": {"provider": "proj", "grid": "egm2008"},
|
|
@@ -454,6 +454,52 @@ class GridWriter:
|
|
|
454
454
|
return filename
|
|
455
455
|
|
|
456
456
|
|
|
457
|
+
def calculate_psmsl_msl(csv_path: str) -> float:
|
|
458
|
+
"""Reads a PSMSL time-series CSV generated by fetchez, filters out
|
|
459
|
+
missing data flags (-99999), and calculates the all-time Mean Sea Level.
|
|
460
|
+
|
|
461
|
+
Returns the MSL value in METERS.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
import csv
|
|
465
|
+
|
|
466
|
+
valid_measurements = []
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
with open(csv_path, "r", encoding="utf-8") as f:
|
|
470
|
+
reader = csv.reader(f, delimiter=";")
|
|
471
|
+
for row in reader:
|
|
472
|
+
if len(row) < 4:
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
# Column 1 is the MSL value in millimeters
|
|
477
|
+
msl_mm = float(row[1].strip())
|
|
478
|
+
|
|
479
|
+
# -99999 is the PSMSL nodata flag
|
|
480
|
+
if msl_mm != -99999.0:
|
|
481
|
+
valid_measurements.append(msl_mm)
|
|
482
|
+
except ValueError:
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
if not valid_measurements:
|
|
486
|
+
logger.warning(f"No valid data found in {csv_path}")
|
|
487
|
+
return np.nan
|
|
488
|
+
|
|
489
|
+
# Calculate the mean in mm, then convert to meters
|
|
490
|
+
mean_mm = sum(valid_measurements) / len(valid_measurements)
|
|
491
|
+
mean_meters = mean_mm / 1000.0
|
|
492
|
+
|
|
493
|
+
logger.info(
|
|
494
|
+
f"Processed {len(valid_measurements)} months of data. MSL: {mean_meters:.3f} m"
|
|
495
|
+
)
|
|
496
|
+
return mean_meters
|
|
497
|
+
|
|
498
|
+
except Exception as e:
|
|
499
|
+
logger.error(f"Failed to process PSMSL file {csv_path}: {e}")
|
|
500
|
+
return np.nan
|
|
501
|
+
|
|
502
|
+
|
|
457
503
|
class GridGen:
|
|
458
504
|
@staticmethod
|
|
459
505
|
def from_stations(
|
|
@@ -463,7 +509,7 @@ class GridGen:
|
|
|
463
509
|
If a station lacks the target datum, it falls back to MSL and uses the
|
|
464
510
|
baseline_grid (FES) to bridge the gap to the geodetic frame.
|
|
465
511
|
"""
|
|
466
|
-
|
|
512
|
+
|
|
467
513
|
import json
|
|
468
514
|
from fetchez.modules.tides import Tides
|
|
469
515
|
import fetchez
|
|
@@ -23,19 +23,41 @@ import zipfile
|
|
|
23
23
|
import numpy as np
|
|
24
24
|
from typing import Tuple
|
|
25
25
|
|
|
26
|
-
from . import utils
|
|
26
|
+
# from . import utils
|
|
27
27
|
from .definitions import Datums # Required for ID lookups
|
|
28
28
|
|
|
29
29
|
logger = logging.getLogger(__name__)
|
|
30
30
|
|
|
31
|
-
ae = ".exe" if sys.platform == "win32" else ""
|
|
32
|
-
# Validating HTDP existence
|
|
33
|
-
htdp_cmd = (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
HAS_HTDP = utils.cmd_check(f"htdp{ae}", htdp_cmd)
|
|
31
|
+
# ae = ".exe" if sys.platform == "win32" else ""
|
|
32
|
+
# # Validating HTDP existence
|
|
33
|
+
# htdp_cmd = (
|
|
34
|
+
# "echo 0 | htdp 2>&1"
|
|
35
|
+
# if sys.platform == "win32"
|
|
36
|
+
# else "echo 0 | htdp 2>&1 | grep SOFTWARE | awk '{print $3}'"
|
|
37
|
+
# )
|
|
38
|
+
# HAS_HTDP = utils.cmd_check(f"htdp{ae}", htdp_cmd)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_htdp_path(version="3.5.0"):
|
|
42
|
+
"""Finds the HTDP executable, prioritizing the local cache over system PATH."""
|
|
43
|
+
|
|
44
|
+
clean_version = version.replace("v", "")
|
|
45
|
+
ae = ".exe" if sys.platform == "win32" else ""
|
|
46
|
+
|
|
47
|
+
# Check local transformez_cache/bin/
|
|
48
|
+
cache_bin = os.path.join(
|
|
49
|
+
os.getcwd(), "transformez_cache", "bin", f"htdp_{clean_version}{ae}"
|
|
50
|
+
)
|
|
51
|
+
if os.path.exists(cache_bin):
|
|
52
|
+
return cache_bin
|
|
53
|
+
|
|
54
|
+
# Check system PATH
|
|
55
|
+
import shutil
|
|
56
|
+
|
|
57
|
+
if shutil.which(f"htdp{ae}"):
|
|
58
|
+
return f"htdp{ae}"
|
|
59
|
+
|
|
60
|
+
return None
|
|
39
61
|
|
|
40
62
|
|
|
41
63
|
class HTDP:
|
|
@@ -47,7 +69,11 @@ class HTDP:
|
|
|
47
69
|
self.htdp_bin = htdp_bin
|
|
48
70
|
self.version = version
|
|
49
71
|
self.verbose = verbose
|
|
50
|
-
|
|
72
|
+
|
|
73
|
+
self.htdp_bin = htdp_bin or resolve_htdp_path(self.version)
|
|
74
|
+
self.has_htdp = self.htdp_bin is not None
|
|
75
|
+
|
|
76
|
+
if not self.has_htdp:
|
|
51
77
|
logger.error(
|
|
52
78
|
f"HTDP {self.version} is not installed or not in PATH. Run 'transformez htdp install --version {self.version}"
|
|
53
79
|
)
|
|
@@ -57,7 +83,7 @@ class HTDP:
|
|
|
57
83
|
Generates a shift grid between two frames.
|
|
58
84
|
"""
|
|
59
85
|
|
|
60
|
-
if not
|
|
86
|
+
if not self.has_htdp:
|
|
61
87
|
logger.warning("HTDP missing. Returning zero shift.")
|
|
62
88
|
return np.zeros((ny, nx))
|
|
63
89
|
|
|
@@ -190,6 +216,7 @@ class HTDP:
|
|
|
190
216
|
|
|
191
217
|
def run_cmd(self, control_fn):
|
|
192
218
|
"""Executes the binary."""
|
|
219
|
+
|
|
193
220
|
try:
|
|
194
221
|
with open(control_fn, "r") as stdin:
|
|
195
222
|
subprocess.run(
|
|
@@ -268,29 +295,26 @@ def install_htdp_binary(version="3.5.0"):
|
|
|
268
295
|
v_tag = f"v{version}" if not version.startswith("v") else version
|
|
269
296
|
clean_version = v_tag.replace("v", "")
|
|
270
297
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
exe_path = os.path.join(cache_dir, f"htdp_{clean_version}.exe")
|
|
275
|
-
|
|
276
|
-
try:
|
|
277
|
-
urllib.request.urlretrieve(url, exe_path)
|
|
278
|
-
logger.info(f"HTDP {clean_version} installed successfully to: {exe_path}")
|
|
279
|
-
except Exception as e:
|
|
280
|
-
logger.error(f"Failed to download {v_tag} from GitHub: {e}")
|
|
281
|
-
|
|
282
|
-
else:
|
|
283
|
-
logger.info(f"Downloading Unix HTDP source code ({v_tag})...")
|
|
284
|
-
url = f"https://github.com/noaa-ngs/HTDP/archive/refs/tags/{v_tag}.zip"
|
|
285
|
-
zip_path = os.path.join(cache_dir, f"htdp_{clean_version}.zip")
|
|
298
|
+
logger.info(f"Downloading HTDP reposity ({v_tag})...")
|
|
299
|
+
url = f"https://github.com/noaa-ngs/HTDP/archive/refs/tags/{v_tag}.zip"
|
|
300
|
+
zip_path = os.path.join(cache_dir, f"htdp_{clean_version}.zip")
|
|
286
301
|
|
|
287
|
-
|
|
288
|
-
|
|
302
|
+
try:
|
|
303
|
+
urllib.request.urlretrieve(url, zip_path)
|
|
289
304
|
|
|
305
|
+
if sys.platform == "win32":
|
|
306
|
+
with zipfile.ZipFile(zip_path, "r") as z:
|
|
307
|
+
exe_name = next(name for name in z.namelist() if name.endswith(".exe"))
|
|
308
|
+
extracted_path = z.extract(exe_name, cache_dir)
|
|
309
|
+
exe_path = os.path.join(cache_dir, f"htdp_{clean_version}.exe")
|
|
310
|
+
|
|
311
|
+
if os.path.exists(exe_path):
|
|
312
|
+
os.remove(exe_path)
|
|
313
|
+
shutil.move(extracted_path, exe_path)
|
|
314
|
+
else:
|
|
290
315
|
extract_dir = os.path.join(cache_dir, f"htdp_src_{clean_version}")
|
|
291
316
|
with zipfile.ZipFile(zip_path, "r") as z:
|
|
292
317
|
z.extractall(extract_dir)
|
|
293
|
-
os.remove(zip_path)
|
|
294
318
|
|
|
295
319
|
# GitHub extracts to a folder named Repository-Tag (e.g., HTDP-3.5.0)
|
|
296
320
|
source_folder = os.path.join(extract_dir, f"HTDP-{clean_version}")
|
|
@@ -308,8 +332,10 @@ def install_htdp_binary(version="3.5.0"):
|
|
|
308
332
|
# Clean up the raw source files
|
|
309
333
|
shutil.rmtree(extract_dir)
|
|
310
334
|
|
|
311
|
-
|
|
312
|
-
|
|
335
|
+
os.remove(zip_path)
|
|
336
|
+
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logger.error(f"Failed to install HTDP {clean_version}: {e}")
|
|
313
339
|
|
|
314
340
|
|
|
315
341
|
def _install_htdp_binary():
|
|
@@ -17,7 +17,7 @@ import logging
|
|
|
17
17
|
from pyproj import CRS, Transformer
|
|
18
18
|
|
|
19
19
|
import numpy as np
|
|
20
|
-
from rasterio.warp import
|
|
20
|
+
from rasterio.warp import reproject, Resampling
|
|
21
21
|
from rasterio.transform import from_bounds
|
|
22
22
|
|
|
23
23
|
from fetchez.spatial import Region
|
|
@@ -184,19 +184,15 @@ class SRSParser:
|
|
|
184
184
|
|
|
185
185
|
# Determine Native WGS84 Region for Transformez
|
|
186
186
|
src_is_projected = self.tc["src_crs"].is_projected
|
|
187
|
+
if not proc_region.srs:
|
|
188
|
+
proc_region.srs = (self.tc["src_crs"],)
|
|
189
|
+
|
|
187
190
|
if src_is_projected:
|
|
188
191
|
# Transform the native bounding box to WGS84 to query the shift models
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"EPSG:4326",
|
|
192
|
-
proc_region.xmin,
|
|
193
|
-
proc_region.ymin,
|
|
194
|
-
proc_region.xmax,
|
|
195
|
-
proc_region.ymax,
|
|
196
|
-
)
|
|
197
|
-
vt_region = Region(w, e, s, n)
|
|
192
|
+
vt_region = proc_region.copy()
|
|
193
|
+
vt_region.warp("EPSG:4326")
|
|
198
194
|
else:
|
|
199
|
-
vt_region = proc_region
|
|
195
|
+
vt_region = proc_region.copy()
|
|
200
196
|
|
|
201
197
|
# Generate grid resolution based on WGS84 bounds (approx 3 arc-seconds)
|
|
202
198
|
inc_deg = 3.0 / 3600.0
|
|
@@ -29,6 +29,12 @@ WGS84_EPSG = 4979
|
|
|
29
29
|
NAD83_EPSG = 6319
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
class MissingGridError(Exception):
|
|
33
|
+
"""Raised when a required shift grid cannot be fetched, is missing, or is persistently corrupted."""
|
|
34
|
+
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
class VerticalTransform:
|
|
33
39
|
"""Generate a vertical transformation grid using Transformez."""
|
|
34
40
|
|
|
@@ -107,7 +113,7 @@ class VerticalTransform:
|
|
|
107
113
|
|
|
108
114
|
files = fetchez.get(
|
|
109
115
|
module=module_name,
|
|
110
|
-
region=self.region
|
|
116
|
+
region=self.region,
|
|
111
117
|
outdir=self.cache_dir,
|
|
112
118
|
threads=2,
|
|
113
119
|
check_size=True,
|
|
@@ -212,7 +218,8 @@ class VerticalTransform:
|
|
|
212
218
|
from .grid_engine import GridCorruptionError
|
|
213
219
|
|
|
214
220
|
if not name:
|
|
215
|
-
return np.zeros((self.ny, self.nx))
|
|
221
|
+
# return np.zeros((self.ny, self.nx))
|
|
222
|
+
raise MissingGridError("A valid grid name must be provided to the fetcher.")
|
|
216
223
|
|
|
217
224
|
if not provider:
|
|
218
225
|
provider = "proj"
|
|
@@ -298,7 +305,30 @@ class VerticalTransform:
|
|
|
298
305
|
files.sort(key=sort_key, reverse=True)
|
|
299
306
|
|
|
300
307
|
if not files:
|
|
301
|
-
return np.zeros((self.ny, self.nx))
|
|
308
|
+
# return np.zeros((self.ny, self.nx))
|
|
309
|
+
if attempt < max_retries - 1:
|
|
310
|
+
logger.warning(
|
|
311
|
+
f"Grid '{name}' not found. Wiping cache for '{provider}' and retrying (Attempt {attempt + 2}/{max_retries})..."
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if os.path.exists(self.cache_dir):
|
|
315
|
+
for f in os.listdir(self.cache_dir):
|
|
316
|
+
if (
|
|
317
|
+
name.lower() in f.lower()
|
|
318
|
+
or provider.lower() in f.lower()
|
|
319
|
+
):
|
|
320
|
+
try:
|
|
321
|
+
os.remove(os.path.join(self.cache_dir, f))
|
|
322
|
+
except OSError:
|
|
323
|
+
pass
|
|
324
|
+
continue
|
|
325
|
+
else:
|
|
326
|
+
logger.error(
|
|
327
|
+
f"FATAL: Max retries exhausted. Could not locate grid '{name}' from '{provider}'."
|
|
328
|
+
)
|
|
329
|
+
raise MissingGridError(
|
|
330
|
+
f"Required shift grid '{name}' is missing or unavailable."
|
|
331
|
+
)
|
|
302
332
|
|
|
303
333
|
try:
|
|
304
334
|
if provider == "seanoe" or provider == "fes":
|
|
@@ -327,11 +357,15 @@ class VerticalTransform:
|
|
|
327
357
|
logger.error(
|
|
328
358
|
"Max retries reached. Could not secure an uncorrupted grid."
|
|
329
359
|
)
|
|
330
|
-
return np.zeros((self.ny, self.nx))
|
|
360
|
+
# return np.zeros((self.ny, self.nx))
|
|
361
|
+
raise MissingGridError(f"Grid '{name}' is persistently corrupted.")
|
|
331
362
|
|
|
332
|
-
return np.zeros((self.ny, self.nx))
|
|
363
|
+
# return np.zeros((self.ny, self.nx))
|
|
364
|
+
raise MissingGridError(
|
|
365
|
+
f"Failed to fetch grid '{name}' due to an unknown error."
|
|
366
|
+
)
|
|
333
367
|
|
|
334
|
-
def _get_htdp_shift(self, epsg_from, epsg_to, epoch_from, epoch_to):
|
|
368
|
+
def _get_htdp_shift(self, epsg_from, epsg_to, epoch_from, epoch_to, context=""):
|
|
335
369
|
"""Calculate Frame Shift via HTDP with Fallback."""
|
|
336
370
|
|
|
337
371
|
if epsg_from == epsg_to and epoch_from == epoch_to:
|
|
@@ -340,7 +374,10 @@ class VerticalTransform:
|
|
|
340
374
|
from . import htdp
|
|
341
375
|
|
|
342
376
|
try:
|
|
343
|
-
|
|
377
|
+
ctx_str = f" {context}" if context else ""
|
|
378
|
+
logger.info(
|
|
379
|
+
f" [HTDP] Frame Shift: EPSG:{epsg_from} -> EPSG:{epsg_to}{ctx_str}"
|
|
380
|
+
)
|
|
344
381
|
tool = htdp.HTDP(version="3.5.0", verbose=False)
|
|
345
382
|
|
|
346
383
|
# Attempt 1: Full Cross-Epoch Shift (Datum Shift + Crustal Velocity)
|
|
@@ -548,7 +585,11 @@ class VerticalTransform:
|
|
|
548
585
|
|
|
549
586
|
if global_shift is not None and np.any(global_shift):
|
|
550
587
|
htdp_wgs_to_nad = self._get_htdp_shift(
|
|
551
|
-
WGS84_EPSG,
|
|
588
|
+
WGS84_EPSG,
|
|
589
|
+
NAD83_EPSG,
|
|
590
|
+
self.epoch_in,
|
|
591
|
+
2010.0,
|
|
592
|
+
context="(Aligning Global Proxy to NAD83)",
|
|
552
593
|
)
|
|
553
594
|
fes_nad83 = global_shift + htdp_wgs_to_nad
|
|
554
595
|
fes_navd88 = fes_nad83 - geoid_grid
|
|
@@ -748,7 +789,11 @@ class VerticalTransform:
|
|
|
748
789
|
if chain_shift is not None:
|
|
749
790
|
if native_epsg != self.hub_epsg:
|
|
750
791
|
htdp_shift = self._get_htdp_shift(
|
|
751
|
-
native_epsg,
|
|
792
|
+
native_epsg,
|
|
793
|
+
self.hub_epsg,
|
|
794
|
+
epoch,
|
|
795
|
+
self.epoch_out,
|
|
796
|
+
context="(Stepping to Central Hub)",
|
|
752
797
|
)
|
|
753
798
|
chain_shift += htdp_shift
|
|
754
799
|
chain_desc += f" + Frame({native_epsg}->{self.hub_epsg})"
|
|
@@ -769,7 +814,11 @@ class VerticalTransform:
|
|
|
769
814
|
|
|
770
815
|
if self.hub_epsg != native_epsg:
|
|
771
816
|
htdp_shift = self._get_htdp_shift(
|
|
772
|
-
self.hub_epsg,
|
|
817
|
+
self.hub_epsg,
|
|
818
|
+
native_epsg,
|
|
819
|
+
self.epoch_in,
|
|
820
|
+
epoch,
|
|
821
|
+
context="(Extracting from Central Hub)",
|
|
773
822
|
)
|
|
774
823
|
total_out += htdp_shift
|
|
775
824
|
desc_parts.append(f"Hub({self.hub_epsg}->{native_epsg})")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|