PyGeoFetch 1.7.0__tar.gz → 1.9.0__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.
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/CHANGELOG.md +2 -2
- {pygeofetch-1.7.0/pygeofetch.egg-info → pygeofetch-1.9.0}/PKG-INFO +72 -3
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/README.md +69 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/__init__.py +1 -1
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/insar/__init__.py +46 -1
- pygeofetch-1.9.0/pygeofetch/insar/annotation.py +153 -0
- pygeofetch-1.9.0/pygeofetch/insar/coregister.py +339 -0
- pygeofetch-1.9.0/pygeofetch/insar/geolocation.py +478 -0
- pygeofetch-1.9.0/pygeofetch/insar/gpu.py +106 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/insar/interferogram.py +174 -19
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/insar/timeseries.py +57 -5
- pygeofetch-1.9.0/pygeofetch/insar/validate.py +251 -0
- pygeofetch-1.9.0/pygeofetch/insar/visualize.py +226 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/meta.yaml +1 -1
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/viz/plot.py +381 -1
- {pygeofetch-1.7.0 → pygeofetch-1.9.0/pygeofetch.egg-info}/PKG-INFO +72 -3
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch.egg-info/SOURCES.txt +6 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch.egg-info/requires.txt +2 -2
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pyproject.toml +3 -3
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/LICENSE +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/MANIFEST.in +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/__init__.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/auth_commands.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/config_commands.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/download_commands.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/index_commands.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/main.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/pipeline_process_commands.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/postprocess_commands.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/preprocess_commands.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/sar_commands.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/cli/search_commands.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/config/__init__.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/config/defaults.yaml +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/config/settings.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/core/__init__.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/core/authenticator.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/core/cache_manager.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/core/downloader.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/core/engine.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/core/logging.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/core/orbits.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/core/scheduler.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/core/searcher.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/insar/atmosphere.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/insar/extraction.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/insar/unwrap.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/models/__init__.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/models/download_task.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/models/satellite_data.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/models/search_query.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/models/user_auth.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processing/__init__.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processing/base.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processing/batch.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processing/indices.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processing/pipeline.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processing/postprocessor.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processing/preprocessor.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processing/sar.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processor/__init__.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processor/indices.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processor/landsat.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processor/loader.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processor/stacker.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/processor/timeseries.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/__init__.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/airbus_oneatlas.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/aws_earth.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/base.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/copernicus.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/digitalglobe.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/earth_explorer_additional.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/element84.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/eodag_provider.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/esa_scihub.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/geoserver_generic.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/google_earth_engine.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/inpe_cbers.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/isro_bhuvan.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/jaxa_earth.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/maxar_gbdx.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/nasa_earthdata.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/noaa_big_data.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/opentopography.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/planet.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/planetary_computer.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/sentinel_hub.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/terrabotics.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/providers/usgs.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/sar/__init__.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/sar/_native.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/sar/_ost.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/sar/_sarxarray.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/sar/extraction.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/sar/processor.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/utils/__init__.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/utils/file_utils.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/utils/gcp_recovery.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/utils/geo_utils.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/utils/logging_setup.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/utils/retry_handler.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/utils/validators.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/viz/__init__.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch/viz/map.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch.egg-info/dependency_links.txt +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch.egg-info/entry_points.txt +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch.egg-info/not-zip-safe +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/pygeofetch.egg-info/top_level.txt +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/setup.cfg +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/tests/test_extraction.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/tests/test_insar.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/tests/test_pgv_integration_contract.py +0 -0
- {pygeofetch-1.7.0 → pygeofetch-1.9.0}/tests/test_viz_plot.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to PyGeoFetch are documented here.
|
|
4
|
-
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.9.0/).
|
|
5
5
|
|
|
6
|
-
## [1.
|
|
6
|
+
## [1.9.0] — 2026-07-12
|
|
7
7
|
|
|
8
8
|
### Added
|
|
9
9
|
- Federated search across 22+ satellite data providers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygeofetch
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Universal satellite data pipeline + geospatial processing platform
|
|
5
5
|
Author-email: Samuel Appiah Kubi <appiahkubis14@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -94,8 +94,8 @@ Requires-Dist: plotly>=5.14.0; extra == "viz"
|
|
|
94
94
|
Requires-Dist: whitebox>=2.3.0; extra == "viz"
|
|
95
95
|
Requires-Dist: whiteboxgui>=2.3.0; extra == "viz"
|
|
96
96
|
Requires-Dist: geemap>=0.28.0; extra == "viz"
|
|
97
|
-
Requires-Dist: osmnx>=1.
|
|
98
|
-
Requires-Dist: contextily>=1.
|
|
97
|
+
Requires-Dist: osmnx>=1.9.0; extra == "viz"
|
|
98
|
+
Requires-Dist: contextily>=1.9.0; extra == "viz"
|
|
99
99
|
Requires-Dist: xyzservices>=2023.10.0; extra == "viz"
|
|
100
100
|
Requires-Dist: mapclassify>=2.5.0; extra == "viz"
|
|
101
101
|
Requires-Dist: geoplot>=0.5.0; extra == "viz"
|
|
@@ -467,6 +467,70 @@ pygeofetch proc-pipeline template change_detection
|
|
|
467
467
|
|
|
468
468
|
---
|
|
469
469
|
|
|
470
|
+
|
|
471
|
+
### 🌐 InSAR Processing (`pygeofetch.insar`)
|
|
472
|
+
|
|
473
|
+
A full Interferometric SAR chain for Sentinel-1 — search through SBAS time series inversion, pure Python, no external InSAR software required beyond `snaphu-py`'s bundled unwrapper.
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
pip install "pygeofetch[insar]" # native SBAS inversion
|
|
477
|
+
pip install "pygeofetch[insar-full]" # + MintPy passthrough for advanced corrections
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
| Component | Description |
|
|
481
|
+
|---|---|
|
|
482
|
+
| `InterferogramGenerator` | Coregistration (real orbit-based, or shape-based fallback) + Enhanced Spectral Diversity (ESD) refinement for TOPS burst continuity + topographic phase removal |
|
|
483
|
+
| `PhaseUnwrapper` | SNAPHU (Chen & Zebker 2001) via the official `snaphu-py` bindings — the same algorithm used by ASF, ISCE2/3, GAMMA, and SNAP |
|
|
484
|
+
| `AtmosphericCorrector` | Elevation-correlated regression or ERA5 reanalysis-based tropospheric delay correction |
|
|
485
|
+
| `SBASTimeSeries` | Small BAseline Subset displacement/velocity inversion (Berardino et al. 2002) |
|
|
486
|
+
| `DataValidator` | SLC, coherence, and SBAS-network sanity checks, wired in at every real pipeline entry point |
|
|
487
|
+
|
|
488
|
+
Real orbit-based coregistration computes genuine per-pixel offsets from actual satellite orbit state vectors and acquisition timing (not a shape-matching guess) — supply a DEM, both SAFE archives, and both `.EOF` orbit files, and it's used automatically; otherwise falls back cleanly to shape-based resampling.
|
|
489
|
+
|
|
490
|
+
Every stage supports automatic visualization (`auto_visualize=True`) and optional GPU acceleration (`use_gpu=True`, CuPy) for coherence estimation and SBAS inversion on large scenes.
|
|
491
|
+
|
|
492
|
+
See [`pygeofetch/insar/README.md`](pygeofetch/insar/README.md) for the full processing chain, verification methodology, and current limitations.
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
```python
|
|
496
|
+
# InSAR — search to SBAS time series
|
|
497
|
+
from pygeofetch.insar import (
|
|
498
|
+
InterferogramGenerator, PhaseUnwrapper, AtmosphericCorrector,
|
|
499
|
+
SBASTimeSeries, DataValidator,
|
|
500
|
+
)
|
|
501
|
+
from pygeofetch.insar.timeseries import InterferogramPair
|
|
502
|
+
from pygeofetch.core.orbits import fetch_orbit_file
|
|
503
|
+
|
|
504
|
+
# Coregistration + interferogram formation (real orbit-based when a DEM,
|
|
505
|
+
# both SAFE archives, and both orbit files are supplied; falls back to
|
|
506
|
+
# shape-based resampling otherwise)
|
|
507
|
+
gen = InterferogramGenerator(coherence_window=5, esd_enabled=True, use_gpu=False)
|
|
508
|
+
result = gen.process_pair(
|
|
509
|
+
reference="slc_ref.tif", secondary="slc_sec.tif", dem="dem.tif",
|
|
510
|
+
reference_safe_zip="ref.SAFE.zip", secondary_safe_zip="sec.SAFE.zip",
|
|
511
|
+
reference_orbit_file=fetch_orbit_file("S1A_..._ref"),
|
|
512
|
+
secondary_orbit_file=fetch_orbit_file("S1A_..._sec"),
|
|
513
|
+
)
|
|
514
|
+
result.save("./output", auto_visualize=True)
|
|
515
|
+
|
|
516
|
+
# Atmospheric correction + phase unwrapping
|
|
517
|
+
atm = AtmosphericCorrector(method="elevation")
|
|
518
|
+
corrected_phase = atm.correct(result.interferogram, dem="dem.tif")
|
|
519
|
+
|
|
520
|
+
unwrapper = PhaseUnwrapper(cost_mode="defo", init_method="mcf")
|
|
521
|
+
unwrapped, conncomp = unwrapper.unwrap(corrected_phase, result.coherence)
|
|
522
|
+
|
|
523
|
+
# SBAS time series inversion across a network of interferograms
|
|
524
|
+
pairs = [
|
|
525
|
+
InterferogramPair("2026-01-01", "2026-01-13", unwrapped, result.coherence),
|
|
526
|
+
# ... additional pairs forming a connected network
|
|
527
|
+
]
|
|
528
|
+
sbas = SBASTimeSeries(wavelength_m=0.05546576, reference_date="2026-01-01")
|
|
529
|
+
ts_result = sbas.invert(pairs, reference_pixel=(10, 15)) # known-stable pixel
|
|
530
|
+
ts_result.save("./timeseries", auto_visualize=True)
|
|
531
|
+
print(f"Mean velocity: {ts_result.velocity.mean()*1000:.1f} mm/year")
|
|
532
|
+
```
|
|
533
|
+
|
|
470
534
|
## 🖥️ Complete CLI Reference
|
|
471
535
|
|
|
472
536
|
```
|
|
@@ -653,3 +717,8 @@ pytest tests/unit/ -v
|
|
|
653
717
|
pygeofetch is free and open source software, licensed under the [MIT License](LICENSE).
|
|
654
718
|
|
|
655
719
|
© 2026 Samuel Appiah Kubi. Part of the **PyGeoVision** platform — [pygeofetch](https://github.com/appiahkubis14/pygeofetch) (data + processing) complete Earth observation pipeline.
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
|
|
@@ -331,6 +331,70 @@ pygeofetch proc-pipeline template change_detection
|
|
|
331
331
|
|
|
332
332
|
---
|
|
333
333
|
|
|
334
|
+
|
|
335
|
+
### 🌐 InSAR Processing (`pygeofetch.insar`)
|
|
336
|
+
|
|
337
|
+
A full Interferometric SAR chain for Sentinel-1 — search through SBAS time series inversion, pure Python, no external InSAR software required beyond `snaphu-py`'s bundled unwrapper.
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
pip install "pygeofetch[insar]" # native SBAS inversion
|
|
341
|
+
pip install "pygeofetch[insar-full]" # + MintPy passthrough for advanced corrections
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
| Component | Description |
|
|
345
|
+
|---|---|
|
|
346
|
+
| `InterferogramGenerator` | Coregistration (real orbit-based, or shape-based fallback) + Enhanced Spectral Diversity (ESD) refinement for TOPS burst continuity + topographic phase removal |
|
|
347
|
+
| `PhaseUnwrapper` | SNAPHU (Chen & Zebker 2001) via the official `snaphu-py` bindings — the same algorithm used by ASF, ISCE2/3, GAMMA, and SNAP |
|
|
348
|
+
| `AtmosphericCorrector` | Elevation-correlated regression or ERA5 reanalysis-based tropospheric delay correction |
|
|
349
|
+
| `SBASTimeSeries` | Small BAseline Subset displacement/velocity inversion (Berardino et al. 2002) |
|
|
350
|
+
| `DataValidator` | SLC, coherence, and SBAS-network sanity checks, wired in at every real pipeline entry point |
|
|
351
|
+
|
|
352
|
+
Real orbit-based coregistration computes genuine per-pixel offsets from actual satellite orbit state vectors and acquisition timing (not a shape-matching guess) — supply a DEM, both SAFE archives, and both `.EOF` orbit files, and it's used automatically; otherwise falls back cleanly to shape-based resampling.
|
|
353
|
+
|
|
354
|
+
Every stage supports automatic visualization (`auto_visualize=True`) and optional GPU acceleration (`use_gpu=True`, CuPy) for coherence estimation and SBAS inversion on large scenes.
|
|
355
|
+
|
|
356
|
+
See [`pygeofetch/insar/README.md`](pygeofetch/insar/README.md) for the full processing chain, verification methodology, and current limitations.
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
# InSAR — search to SBAS time series
|
|
361
|
+
from pygeofetch.insar import (
|
|
362
|
+
InterferogramGenerator, PhaseUnwrapper, AtmosphericCorrector,
|
|
363
|
+
SBASTimeSeries, DataValidator,
|
|
364
|
+
)
|
|
365
|
+
from pygeofetch.insar.timeseries import InterferogramPair
|
|
366
|
+
from pygeofetch.core.orbits import fetch_orbit_file
|
|
367
|
+
|
|
368
|
+
# Coregistration + interferogram formation (real orbit-based when a DEM,
|
|
369
|
+
# both SAFE archives, and both orbit files are supplied; falls back to
|
|
370
|
+
# shape-based resampling otherwise)
|
|
371
|
+
gen = InterferogramGenerator(coherence_window=5, esd_enabled=True, use_gpu=False)
|
|
372
|
+
result = gen.process_pair(
|
|
373
|
+
reference="slc_ref.tif", secondary="slc_sec.tif", dem="dem.tif",
|
|
374
|
+
reference_safe_zip="ref.SAFE.zip", secondary_safe_zip="sec.SAFE.zip",
|
|
375
|
+
reference_orbit_file=fetch_orbit_file("S1A_..._ref"),
|
|
376
|
+
secondary_orbit_file=fetch_orbit_file("S1A_..._sec"),
|
|
377
|
+
)
|
|
378
|
+
result.save("./output", auto_visualize=True)
|
|
379
|
+
|
|
380
|
+
# Atmospheric correction + phase unwrapping
|
|
381
|
+
atm = AtmosphericCorrector(method="elevation")
|
|
382
|
+
corrected_phase = atm.correct(result.interferogram, dem="dem.tif")
|
|
383
|
+
|
|
384
|
+
unwrapper = PhaseUnwrapper(cost_mode="defo", init_method="mcf")
|
|
385
|
+
unwrapped, conncomp = unwrapper.unwrap(corrected_phase, result.coherence)
|
|
386
|
+
|
|
387
|
+
# SBAS time series inversion across a network of interferograms
|
|
388
|
+
pairs = [
|
|
389
|
+
InterferogramPair("2026-01-01", "2026-01-13", unwrapped, result.coherence),
|
|
390
|
+
# ... additional pairs forming a connected network
|
|
391
|
+
]
|
|
392
|
+
sbas = SBASTimeSeries(wavelength_m=0.05546576, reference_date="2026-01-01")
|
|
393
|
+
ts_result = sbas.invert(pairs, reference_pixel=(10, 15)) # known-stable pixel
|
|
394
|
+
ts_result.save("./timeseries", auto_visualize=True)
|
|
395
|
+
print(f"Mean velocity: {ts_result.velocity.mean()*1000:.1f} mm/year")
|
|
396
|
+
```
|
|
397
|
+
|
|
334
398
|
## 🖥️ Complete CLI Reference
|
|
335
399
|
|
|
336
400
|
```
|
|
@@ -517,3 +581,8 @@ pytest tests/unit/ -v
|
|
|
517
581
|
pygeofetch is free and open source software, licensed under the [MIT License](LICENSE).
|
|
518
582
|
|
|
519
583
|
© 2026 Samuel Appiah Kubi. Part of the **PyGeoVision** platform — [pygeofetch](https://github.com/appiahkubis14/pygeofetch) (data + processing) complete Earth observation pipeline.
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
|
|
@@ -52,9 +52,39 @@ Usage::
|
|
|
52
52
|
|
|
53
53
|
from pygeofetch.insar.atmosphere import AtmosphericCorrector
|
|
54
54
|
from pygeofetch.insar.extraction import SLCExtractor
|
|
55
|
+
from pygeofetch.insar.gpu import gpu_available
|
|
55
56
|
from pygeofetch.insar.interferogram import InterferogramGenerator, InterferogramResult
|
|
56
57
|
from pygeofetch.insar.timeseries import SBASTimeSeries
|
|
57
58
|
from pygeofetch.insar.unwrap import PhaseUnwrapper
|
|
59
|
+
from pygeofetch.insar.validate import DataValidator, ValidationResult
|
|
60
|
+
from pygeofetch.insar.visualize import (
|
|
61
|
+
visualize_interferogram,
|
|
62
|
+
visualize_timeseries,
|
|
63
|
+
visualize_unwrapped,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Real, orbit-based coregistration components. All exported here are
|
|
67
|
+
# individually verified against known ground truth (see each module's
|
|
68
|
+
# docstring for specifics): annotation.py against ESA's own field-path
|
|
69
|
+
# spec; orbit parsing/interpolation against exact reference values;
|
|
70
|
+
# find_zero_doppler_time against known times from starting guesses up
|
|
71
|
+
# to 30 seconds off; geodetic_to_ecef via round-trip verification.
|
|
72
|
+
# solve_ground_point (in geolocation.py) is deliberately NOT exported
|
|
73
|
+
# here — it has a known, unresolved reliability gap and is not
|
|
74
|
+
# recommended as a primary coregistration path; use
|
|
75
|
+
# compute_offset_field_from_dem() instead, which avoids it entirely.
|
|
76
|
+
from pygeofetch.insar.annotation import SLCGeometry, parse_slc_geometry
|
|
77
|
+
from pygeofetch.insar.geolocation import (
|
|
78
|
+
geodetic_to_ecef,
|
|
79
|
+
find_zero_doppler_time,
|
|
80
|
+
parse_orbit_file,
|
|
81
|
+
interpolate_orbit_state,
|
|
82
|
+
)
|
|
83
|
+
from pygeofetch.insar.coregister import (
|
|
84
|
+
compute_offset_field_from_dem,
|
|
85
|
+
fit_offset_polynomial,
|
|
86
|
+
resample_with_offset_field,
|
|
87
|
+
)
|
|
58
88
|
|
|
59
89
|
__all__ = [
|
|
60
90
|
"InterferogramGenerator",
|
|
@@ -63,4 +93,19 @@ __all__ = [
|
|
|
63
93
|
"SBASTimeSeries",
|
|
64
94
|
"AtmosphericCorrector",
|
|
65
95
|
"SLCExtractor",
|
|
66
|
-
|
|
96
|
+
"DataValidator",
|
|
97
|
+
"ValidationResult",
|
|
98
|
+
"gpu_available",
|
|
99
|
+
"visualize_interferogram",
|
|
100
|
+
"visualize_unwrapped",
|
|
101
|
+
"visualize_timeseries",
|
|
102
|
+
"SLCGeometry",
|
|
103
|
+
"parse_slc_geometry",
|
|
104
|
+
"geodetic_to_ecef",
|
|
105
|
+
"find_zero_doppler_time",
|
|
106
|
+
"parse_orbit_file",
|
|
107
|
+
"interpolate_orbit_state",
|
|
108
|
+
"compute_offset_field_from_dem",
|
|
109
|
+
"fit_offset_polynomial",
|
|
110
|
+
"resample_with_offset_field",
|
|
111
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sentinel-1 annotation XML parsing — real per-pixel acquisition timing.
|
|
3
|
+
|
|
4
|
+
This is the missing link between "a pixel at (row, col) in an extracted
|
|
5
|
+
SLC GeoTIFF" and "the exact acquisition time and slant range needed to
|
|
6
|
+
feed geolocation.py's orbit-based solver" — that information does not
|
|
7
|
+
exist in the GeoTIFF itself (a general raster format); it lives in a
|
|
8
|
+
separate XML file inside the .SAFE archive.
|
|
9
|
+
|
|
10
|
+
Field paths confirmed against ESA's own Mission Performance Centre
|
|
11
|
+
documentation (Thermal Denoising of Products Generated by the S-1 IPF,
|
|
12
|
+
MPC-0392):
|
|
13
|
+
|
|
14
|
+
/product/imageAnnotation/imageInformation/productFirstLineUtcTime
|
|
15
|
+
/product/imageAnnotation/imageInformation/azimuthTimeInterval
|
|
16
|
+
/product/imageAnnotation/imageInformation/slantRangeTime
|
|
17
|
+
/product/generalAnnotation/productInformation/rangeSamplingRate
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import zipfile
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from datetime import datetime, timedelta
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Optional, Union
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("pygeofetch.insar.annotation")
|
|
30
|
+
|
|
31
|
+
SPEED_OF_LIGHT = 299792458.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class SLCGeometry:
|
|
36
|
+
"""
|
|
37
|
+
Real per-pixel acquisition geometry for one Sentinel-1 SLC measurement,
|
|
38
|
+
parsed from its annotation XML — everything needed to convert a pixel
|
|
39
|
+
(row, col) into an exact acquisition time and slant range.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
first_line_time: datetime
|
|
43
|
+
azimuth_time_interval_s: float
|
|
44
|
+
near_range_time_s: float
|
|
45
|
+
range_sampling_rate_hz: float
|
|
46
|
+
n_lines: int
|
|
47
|
+
n_columns: int
|
|
48
|
+
|
|
49
|
+
def azimuth_time(self, row: float) -> datetime:
|
|
50
|
+
"""Exact acquisition time of a given (possibly fractional) row."""
|
|
51
|
+
return self.first_line_time + timedelta(
|
|
52
|
+
seconds=row * self.azimuth_time_interval_s
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def range_time(self, col: float) -> float:
|
|
56
|
+
"""Two-way slant range time (seconds) of a given (possibly
|
|
57
|
+
fractional) column."""
|
|
58
|
+
return self.near_range_time_s + col / self.range_sampling_rate_hz
|
|
59
|
+
|
|
60
|
+
def slant_range_m(self, col: float) -> float:
|
|
61
|
+
"""One-way slant range (metres) of a given column."""
|
|
62
|
+
return self.range_time(col) * SPEED_OF_LIGHT / 2.0
|
|
63
|
+
|
|
64
|
+
def row_for_azimuth_time(self, t: datetime) -> float:
|
|
65
|
+
"""Inverse of azimuth_time(): which (fractional) row corresponds
|
|
66
|
+
to a given acquisition time."""
|
|
67
|
+
return (t - self.first_line_time).total_seconds() / self.azimuth_time_interval_s
|
|
68
|
+
|
|
69
|
+
def col_for_range_time(self, range_time_s: float) -> float:
|
|
70
|
+
"""Inverse of range_time(): which (fractional) column corresponds
|
|
71
|
+
to a given two-way range time."""
|
|
72
|
+
return (range_time_s - self.near_range_time_s) * self.range_sampling_rate_hz
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_slc_geometry(
|
|
76
|
+
safe_zip_path: Union[str, Path], member_hint: Optional[str] = None
|
|
77
|
+
) -> SLCGeometry:
|
|
78
|
+
"""
|
|
79
|
+
Parse real per-pixel acquisition geometry from a Sentinel-1 SAFE
|
|
80
|
+
archive's annotation XML.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
safe_zip_path: Path to the downloaded .SAFE.zip archive.
|
|
84
|
+
member_hint: Optional substring to disambiguate which
|
|
85
|
+
annotation file to use when multiple sub-swaths/
|
|
86
|
+
polarisations are present (e.g. "iw2-vv" or the
|
|
87
|
+
measurement filename this geometry corresponds
|
|
88
|
+
to). If None, uses the first annotation XML found.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
A real SLCGeometry with the exact timing needed to feed
|
|
92
|
+
geolocation.py's solve_ground_point().
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError if no annotation XML is found, or a required field
|
|
96
|
+
is missing — surfaced clearly rather than returning a
|
|
97
|
+
geometry built from guessed/default values.
|
|
98
|
+
"""
|
|
99
|
+
import xml.etree.ElementTree as ET
|
|
100
|
+
|
|
101
|
+
with zipfile.ZipFile(safe_zip_path) as zf:
|
|
102
|
+
candidates = [
|
|
103
|
+
n for n in zf.namelist()
|
|
104
|
+
if "/annotation/" in n and n.lower().endswith(".xml")
|
|
105
|
+
and "/calibration/" not in n.lower()
|
|
106
|
+
]
|
|
107
|
+
if member_hint:
|
|
108
|
+
filtered = [n for n in candidates if member_hint.lower() in n.lower()]
|
|
109
|
+
if filtered:
|
|
110
|
+
candidates = filtered
|
|
111
|
+
|
|
112
|
+
if not candidates:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"{safe_zip_path}: no annotation XML found — this doesn't "
|
|
115
|
+
f"look like a real Sentinel-1 SAFE archive, or the "
|
|
116
|
+
f"expected internal structure has changed."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
with zf.open(candidates[0]) as f:
|
|
120
|
+
root = ET.parse(f).getroot()
|
|
121
|
+
|
|
122
|
+
def get_text(path: str) -> str:
|
|
123
|
+
elem = root.find(path)
|
|
124
|
+
if elem is None or elem.text is None:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"{safe_zip_path}: required annotation field missing: {path}"
|
|
127
|
+
)
|
|
128
|
+
return elem.text
|
|
129
|
+
|
|
130
|
+
first_line_str = get_text(".//imageAnnotation/imageInformation/productFirstLineUtcTime")
|
|
131
|
+
first_line_time = datetime.fromisoformat(first_line_str)
|
|
132
|
+
|
|
133
|
+
geometry = SLCGeometry(
|
|
134
|
+
first_line_time=first_line_time,
|
|
135
|
+
azimuth_time_interval_s=float(
|
|
136
|
+
get_text(".//imageAnnotation/imageInformation/azimuthTimeInterval")
|
|
137
|
+
),
|
|
138
|
+
near_range_time_s=float(
|
|
139
|
+
get_text(".//imageAnnotation/imageInformation/slantRangeTime")
|
|
140
|
+
),
|
|
141
|
+
range_sampling_rate_hz=float(
|
|
142
|
+
get_text(".//generalAnnotation/productInformation/rangeSamplingRate")
|
|
143
|
+
),
|
|
144
|
+
n_lines=int(get_text(".//imageAnnotation/imageInformation/numberOfLines")),
|
|
145
|
+
n_columns=int(get_text(".//imageAnnotation/imageInformation/numberOfColumns")),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
logger.info(
|
|
149
|
+
"Parsed real SLC geometry from %s: %d x %d, starting %s",
|
|
150
|
+
Path(safe_zip_path).name, geometry.n_lines, geometry.n_columns,
|
|
151
|
+
geometry.first_line_time,
|
|
152
|
+
)
|
|
153
|
+
return geometry
|