pygeofetch 2.3.0__tar.gz → 2.5.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-2.3.0 → pygeofetch-2.5.0}/CHANGELOG.md +6 -6
- {pygeofetch-2.3.0/pygeofetch.egg-info → pygeofetch-2.5.0}/PKG-INFO +3 -3
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/__init__.py +2 -2
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/main.py +2 -2
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/preflight.py +113 -49
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/unwrap.py +95 -8
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/meta.yaml +1 -1
- {pygeofetch-2.3.0 → pygeofetch-2.5.0/pygeofetch.egg-info}/PKG-INFO +3 -3
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/requires.txt +2 -2
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pyproject.toml +3 -3
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_pgv_integration_contract.py +1 -1
- pygeofetch-2.5.0/tests/test_preflight_gate.py +588 -0
- pygeofetch-2.3.0/tests/test_preflight_gate.py +0 -176
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/LICENSE +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/MANIFEST.in +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/README.md +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/auth_commands.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/config_commands.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/download_commands.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/index_commands.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/pipeline_process_commands.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/postprocess_commands.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/preprocess_commands.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/sar_commands.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/search_commands.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/config/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/config/defaults.yaml +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/config/settings.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/authenticator.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/cache_manager.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/downloader.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/engine.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/logging.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/orbits.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/scheduler.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/searcher.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/analysis.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/annotation.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/atmosphere.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/coregister.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/deburst.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/esd.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/extraction.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/flatearth.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/geolocation.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/gpu.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/interferogram.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/ionosphere.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/provenance.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/splitspectrum_diagnostic.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/stack_selection.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/synthetic.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/timeseries.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/validate.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/visualize.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/workflow.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/models/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/models/download_task.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/models/satellite_data.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/models/search_query.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/models/user_auth.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/base.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/batch.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/indices.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/pipeline.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/postprocessor.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/preprocessor.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/sar.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/indices.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/landsat.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/loader.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/stacker.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/timeseries.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/airbus_oneatlas.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/aws_earth.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/base.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/copernicus.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/copernicus_nodes.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/digitalglobe.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/earth_explorer_additional.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/element84.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/eodag_provider.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/esa_scihub.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/geoserver_generic.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/google_earth_engine.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/inpe_cbers.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/isro_bhuvan.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/jaxa_earth.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/maxar_gbdx.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/nasa_earthdata.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/noaa_big_data.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/opentopography.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/planet.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/planetary_computer.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/sentinel_hub.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/terrabotics.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/usgs.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/_native.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/_ost.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/_sarxarray.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/extraction.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/processor.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/file_utils.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/gcp_recovery.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/geo_utils.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/logging_setup.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/retry_handler.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/validators.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/viz/__init__.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/viz/map.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/viz/plot.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/SOURCES.txt +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/dependency_links.txt +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/entry_points.txt +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/not-zip-safe +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/top_level.txt +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/setup.cfg +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_atmospheric_circular_regression (1).py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_atmospheric_circular_regression.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_build_sbas_network.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_burst_metadata.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_burst_processing_integration.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_burst_sync_large_gap.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_burst_sync_prescreening.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_burst_synchronization.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_copernicus_nodes.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_coregister_integration.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_coregister_upgrades.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_era5_atmospheric_correction.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_extraction.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_flat_earth_phase.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_gcp_reprojection.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_insar.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_ionosphere.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_map_preview_consolidation.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_models.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_preflight_gate (1).py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_provider_geometry_audit (1).py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_provider_geometry_audit.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_provider_scene_disambiguation.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_providers.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_raster_collocation_integration.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_reference_pixel_and_despike.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_risk_mapping.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_screen_stack_orchestration.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_search_download.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_splitspectrum_diagnostic.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_stack_selection_consolidation.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_topo_r2_bug.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_utils.py +0 -0
- {pygeofetch-2.3.0 → pygeofetch-2.5.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/2.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/2.5.0/).
|
|
5
5
|
|
|
6
|
-
## [2.
|
|
6
|
+
## [2.5.0] — 2026-07-29
|
|
7
7
|
|
|
8
8
|
### Added
|
|
9
9
|
- Major SNAPHU unwrapping improvements for urban InSAR applications
|
|
@@ -51,7 +51,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/2.3.0/).
|
|
|
51
51
|
|
|
52
52
|
### Migration Guide
|
|
53
53
|
```python
|
|
54
|
-
# Before (v2.
|
|
54
|
+
# Before (v2.4.0)
|
|
55
55
|
unwrapper = PhaseUnwrapper(cost_mode="defo", init_method="mcf")
|
|
56
56
|
unwrapped = unwrapper.unwrap(phase, coherence, nlooks=2.0)
|
|
57
57
|
|
|
@@ -72,7 +72,7 @@ unwrapped = unwrapper.unwrap(
|
|
|
72
72
|
|
|
73
73
|
---
|
|
74
74
|
|
|
75
|
-
## [2.
|
|
75
|
+
## [2.5.0] — 2026-07-12
|
|
76
76
|
|
|
77
77
|
### Added
|
|
78
78
|
- Federated search across 22+ satellite data providers
|
|
@@ -188,8 +188,8 @@ unwrapped = unwrapper.unwrap(
|
|
|
188
188
|
|
|
189
189
|
| Version | Date | Key Changes |
|
|
190
190
|
|---------|------------|-------------|
|
|
191
|
-
| **2.
|
|
192
|
-
| **2.
|
|
191
|
+
| **2.5.0** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
|
|
192
|
+
| **2.5.0** | 2026-07-12 | 22 providers, 17 indices, Sentinel-1C/D |
|
|
193
193
|
| **1.9.2** | 2026-06-15 | Batch processing, parallel downloads |
|
|
194
194
|
| **1.9.1** | 2026-05-20 | 3D terrain, SNAPHU integration, time series |
|
|
195
195
|
| **1.9.0** | 2026-04-25 | Initial public release |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygeofetch
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -91,8 +91,8 @@ Requires-Dist: sidecar>=0.6.0; extra == "viz"
|
|
|
91
91
|
Requires-Dist: bokeh>=3.0.0; extra == "viz"
|
|
92
92
|
Requires-Dist: seaborn>=0.12.0; extra == "viz"
|
|
93
93
|
Requires-Dist: plotly>=5.14.0; extra == "viz"
|
|
94
|
-
Requires-Dist: whitebox>=2.
|
|
95
|
-
Requires-Dist: whiteboxgui>=2.
|
|
94
|
+
Requires-Dist: whitebox>=2.4.0; extra == "viz"
|
|
95
|
+
Requires-Dist: whiteboxgui>=2.4.0; extra == "viz"
|
|
96
96
|
Requires-Dist: geemap>=0.28.0; extra == "viz"
|
|
97
97
|
Requires-Dist: osmnx>=1.9.4; extra == "viz"
|
|
98
98
|
Requires-Dist: contextily>=1.9.4; extra == "viz"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
PyGeoFetch v2.
|
|
2
|
+
PyGeoFetch v2.4.0 — Universal Satellite Data Pipeline.
|
|
3
3
|
|
|
4
4
|
Unified access to 22+ satellite data providers with a consistent
|
|
5
5
|
Python API and CLI. Supports authentication management, federated search,
|
|
@@ -30,7 +30,7 @@ CLI::
|
|
|
30
30
|
pygeofetch doctor
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
__version__ = "2.
|
|
33
|
+
__version__ = "2.5.0"
|
|
34
34
|
__author__ = "PyGeoFetch Contributors"
|
|
35
35
|
__license__ = "MIT"
|
|
36
36
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
PyGeoFetch CLI v2.
|
|
2
|
+
PyGeoFetch CLI v2.4.0 main entry point.
|
|
3
3
|
|
|
4
4
|
Full command reference:
|
|
5
5
|
pygeofetch auth — credential management
|
|
@@ -71,7 +71,7 @@ def cli(
|
|
|
71
71
|
) -> None:
|
|
72
72
|
"""
|
|
73
73
|
\b
|
|
74
|
-
PyGeoFetch v2.
|
|
74
|
+
PyGeoFetch v2.4.0 — Universal Satellite Data Pipeline
|
|
75
75
|
===========================================================
|
|
76
76
|
Unified access to 22+ satellite data providers.
|
|
77
77
|
|
|
@@ -1,38 +1,34 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Pre-download data-quality gate for InSAR stack selection.
|
|
3
|
-
|
|
4
3
|
Runs AFTER search, BEFORE download. Catches — and where possible
|
|
5
4
|
auto-corrects — real, confirmed data-selection failures this project
|
|
6
5
|
has hit, so bandwidth and hours are never spent on a stack that was
|
|
7
6
|
guaranteed to fail:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
that claimed to be one anyway.
|
|
28
|
-
|
|
7
|
+
Search truncation -> auto-widen max_results & re-search
|
|
8
|
+
Mixed relative orbits -> confirmed (already enforced by
|
|
9
|
+
select_consistent_geometry(), which
|
|
10
|
+
should already have run before this
|
|
11
|
+
gate; re-checked here defensively)
|
|
12
|
+
Multi-candidate per date -> coverage-aware dedup (already
|
|
13
|
+
handled by search_and_select_
|
|
14
|
+
consistent_stack(), which this gate
|
|
15
|
+
calls)
|
|
16
|
+
0%/low AOI coverage -> drop + report genuine gaps
|
|
17
|
+
Sparse temporal sampling -> predict SBAS connectivity, warn
|
|
18
|
+
Burst-timing family risk -> honest advisory, NOT a detector --
|
|
19
|
+
see _screen_burst_families()'s own
|
|
20
|
+
docstring for why a real pre-
|
|
21
|
+
download detector isn't achievable
|
|
22
|
+
with what's available before
|
|
23
|
+
download, and for a real, confirmed
|
|
24
|
+
bug found in an earlier version
|
|
25
|
+
that claimed to be one anyway.
|
|
29
26
|
The definitive burst-family check still happens post-download via
|
|
30
27
|
select_burst_synchronized_dates() (it needs SwathTiming parsed from
|
|
31
28
|
each product's real annotation XML, which isn't available before
|
|
32
29
|
download). This gate handles what real pre-download signals actually
|
|
33
30
|
allow, and is explicit about the one real risk it cannot resolve early.
|
|
34
31
|
"""
|
|
35
|
-
|
|
36
32
|
from __future__ import annotations
|
|
37
33
|
|
|
38
34
|
import logging
|
|
@@ -44,16 +40,14 @@ from shapely.geometry import shape, box
|
|
|
44
40
|
|
|
45
41
|
logger = logging.getLogger("pygeofetch.insar.preflight")
|
|
46
42
|
|
|
47
|
-
|
|
48
43
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
49
44
|
# Report data model
|
|
50
|
-
#
|
|
45
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
51
46
|
|
|
52
47
|
SEVERITY_GREEN = "GREEN" # safe to proceed
|
|
53
48
|
SEVERITY_YELLOW = "YELLOW" # proceed, but with logged caveats
|
|
54
49
|
SEVERITY_RED = "RED" # do not download; fix first
|
|
55
50
|
|
|
56
|
-
|
|
57
51
|
@dataclass
|
|
58
52
|
class PreflightIssue:
|
|
59
53
|
"""A single problem found during preflight, with its fix."""
|
|
@@ -63,12 +57,10 @@ class PreflightIssue:
|
|
|
63
57
|
auto_fixed: bool = False # did the gate heal this automatically?
|
|
64
58
|
fix_detail: str = "" # what the auto-fix did, if applied
|
|
65
59
|
|
|
66
|
-
|
|
67
60
|
@dataclass
|
|
68
61
|
class PreflightReport:
|
|
69
62
|
"""
|
|
70
63
|
Full outcome of the pre-download gate.
|
|
71
|
-
|
|
72
64
|
Carries the go/no-go decision, every issue found, every auto-fix
|
|
73
65
|
applied, and — critically — the provenance manifest that makes the
|
|
74
66
|
whole run reproducible and defensible.
|
|
@@ -78,16 +70,45 @@ class PreflightReport:
|
|
|
78
70
|
issues: List[PreflightIssue] = field(default_factory=list)
|
|
79
71
|
selected: List[Any] = field(default_factory=list) # final scene objects
|
|
80
72
|
manifest: Dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
original_count: int = 0 # how many scenes came IN, before any filtering -- see summary()'s own real-bug note
|
|
81
74
|
|
|
82
75
|
def summary(self) -> str:
|
|
83
76
|
lines = [f"PREFLIGHT {self.severity} — {'PROCEED' if self.go else 'BLOCKED'}"]
|
|
77
|
+
|
|
78
|
+
# Print Bandwidth Manifest if available
|
|
79
|
+
if "bandwidth_and_manifest" in self.manifest:
|
|
80
|
+
bw = self.manifest["bandwidth_and_manifest"]
|
|
81
|
+
lines.append(f" [INFO] Estimated download size: {bw['estimated_bandwidth_gb']} GB for {len(bw['download_manifest'])} scenes.")
|
|
82
|
+
|
|
84
83
|
for issue in self.issues:
|
|
85
84
|
fixed = " [AUTO-FIXED]" if issue.auto_fixed else ""
|
|
86
85
|
lines.append(f" [{issue.severity}] {issue.code}{fixed}: {issue.message}")
|
|
87
86
|
if issue.auto_fixed and issue.fix_detail:
|
|
88
87
|
lines.append(f" -> {issue.fix_detail}")
|
|
89
|
-
return "\n".join(lines)
|
|
90
88
|
|
|
89
|
+
# REAL BUG FOUND AND GUARDED AGAINST HERE: a real notebook
|
|
90
|
+
# called gate.run(selected, search_report) and printed
|
|
91
|
+
# report.summary() -- which correctly showed real dates being
|
|
92
|
+
# excluded -- but never reassigned `selected = report.selected`
|
|
93
|
+
# afterward, so every downstream step (the very next cell,
|
|
94
|
+
# literally titled "Download the real, filtered scenes") kept
|
|
95
|
+
# using the original, unfiltered list. The filtering logic was
|
|
96
|
+
# completely correct; the scenes it said to exclude got
|
|
97
|
+
# downloaded anyway, because nothing after this function call
|
|
98
|
+
# actually used its return value. This can't be fixed from
|
|
99
|
+
# inside this class (a function can't force a caller to use
|
|
100
|
+
# its return value) but it CAN be made loud and impossible to
|
|
101
|
+
# miss in the one place every real run already prints.
|
|
102
|
+
if self.original_count and len(self.selected) < self.original_count:
|
|
103
|
+
dropped = self.original_count - len(self.selected)
|
|
104
|
+
lines.append(
|
|
105
|
+
f" [REMINDER] {dropped} of {self.original_count} scene(s) were "
|
|
106
|
+
f"excluded by this gate. Use report.selected -- not your original "
|
|
107
|
+
f"list -- for every downstream step (download, extraction, "
|
|
108
|
+
f"processing). Passing the original list silently re-includes "
|
|
109
|
+
f"exactly what this gate just excluded."
|
|
110
|
+
)
|
|
111
|
+
return "\n".join(lines)
|
|
91
112
|
|
|
92
113
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
93
114
|
# The gate itself
|
|
@@ -96,7 +117,6 @@ class PreflightReport:
|
|
|
96
117
|
class PreflightGate:
|
|
97
118
|
"""
|
|
98
119
|
Orchestrates every pre-download check and auto-heals what it can.
|
|
99
|
-
|
|
100
120
|
Usage:
|
|
101
121
|
gate = PreflightGate(client, aoi_bbox, start_date, end_date)
|
|
102
122
|
report = gate.run(selected, search_report)
|
|
@@ -105,7 +125,6 @@ class PreflightGate:
|
|
|
105
125
|
raise RuntimeError("Preflight blocked the stack. See report.")
|
|
106
126
|
download_results = client.download(report.selected, ...)
|
|
107
127
|
"""
|
|
108
|
-
|
|
109
128
|
def __init__(
|
|
110
129
|
self,
|
|
111
130
|
client: Any,
|
|
@@ -162,7 +181,7 @@ class PreflightGate:
|
|
|
162
181
|
# authenticated provider these real annotation fetches need.
|
|
163
182
|
self.attempt_real_burst_check = attempt_real_burst_check
|
|
164
183
|
|
|
165
|
-
# ── public entry point
|
|
184
|
+
# ── public entry point ───────────────────────────────────────────────
|
|
166
185
|
def run(self, selected: List[Any], search_report: Dict[str, Any]) -> PreflightReport:
|
|
167
186
|
issues: List[PreflightIssue] = []
|
|
168
187
|
selected = list(selected) # work on a copy
|
|
@@ -172,6 +191,15 @@ class PreflightGate:
|
|
|
172
191
|
if trunc_issue:
|
|
173
192
|
issues.append(trunc_issue)
|
|
174
193
|
|
|
194
|
+
# Baseline for the "did anything get silently dropped downstream"
|
|
195
|
+
# reminder in summary() -- captured HERE, after truncation
|
|
196
|
+
# resolves, not at the very top: truncation can legitimately
|
|
197
|
+
# GROW the candidate pool (a wider re-search finding more real
|
|
198
|
+
# scenes), which isn't a "drop" and shouldn't suppress a real
|
|
199
|
+
# warning about coverage/burst-family filtering that happens
|
|
200
|
+
# after this point.
|
|
201
|
+
original_count = len(selected)
|
|
202
|
+
|
|
175
203
|
# Check 2: AOI coverage per scene (drop genuine gaps)
|
|
176
204
|
selected, cov_issues = self._check_coverage(selected)
|
|
177
205
|
issues.extend(cov_issues)
|
|
@@ -180,7 +208,6 @@ class PreflightGate:
|
|
|
180
208
|
issues.extend(self._check_temporal_network(selected))
|
|
181
209
|
|
|
182
210
|
# Check 4: burst-timing family proxy screen
|
|
183
|
-
|
|
184
211
|
geometry_report = search_report.get("geometry_report", {})
|
|
185
212
|
selected, burst_issues = self._screen_burst_families(selected, geometry_report)
|
|
186
213
|
issues.extend(burst_issues)
|
|
@@ -195,6 +222,7 @@ class PreflightGate:
|
|
|
195
222
|
report = PreflightReport(
|
|
196
223
|
go=go, severity=severity, issues=issues,
|
|
197
224
|
selected=selected, manifest=manifest,
|
|
225
|
+
original_count=original_count,
|
|
198
226
|
)
|
|
199
227
|
logger.info("\n%s", report.summary())
|
|
200
228
|
return report
|
|
@@ -294,7 +322,6 @@ class PreflightGate:
|
|
|
294
322
|
candidate_pairs = generate_candidate_pairs(
|
|
295
323
|
dates, max_temporal_baseline_days=self.max_temporal_baseline_days,
|
|
296
324
|
)
|
|
297
|
-
|
|
298
325
|
adjacency: Dict[str, set] = {d: set() for d in dates}
|
|
299
326
|
for d1, d2 in candidate_pairs:
|
|
300
327
|
adjacency[d1].add(d2)
|
|
@@ -309,8 +336,8 @@ class PreflightGate:
|
|
|
309
336
|
for nb in adjacency[d] - visited:
|
|
310
337
|
visited.add(nb)
|
|
311
338
|
stack.append(nb)
|
|
312
|
-
unconnected = set(dates) - visited
|
|
313
339
|
|
|
340
|
+
unconnected = set(dates) - visited
|
|
314
341
|
issues = []
|
|
315
342
|
if unconnected:
|
|
316
343
|
issues.append(PreflightIssue(
|
|
@@ -351,7 +378,6 @@ class PreflightGate:
|
|
|
351
378
|
full products), falling back to an honest "unassessed" advisory
|
|
352
379
|
only if that isn't available or doesn't succeed. Never silently
|
|
353
380
|
claims risk is absent just because it couldn't be checked.
|
|
354
|
-
|
|
355
381
|
REAL BUG FIXED: earlier versions returned only `issues`, never
|
|
356
382
|
`selected` -- so a successful real detection's own
|
|
357
383
|
"BURST_FAMILY_DETECTED" issue was marked auto_fixed=True with a
|
|
@@ -366,7 +392,6 @@ class PreflightGate:
|
|
|
366
392
|
report claimed the exclusion had already happened. Now returns
|
|
367
393
|
the real, actually-filtered selection, matching the pattern
|
|
368
394
|
_check_coverage() already used correctly.
|
|
369
|
-
|
|
370
395
|
HISTORY, kept because it explains why this function is careful
|
|
371
396
|
about false passes: an earlier version grouped scenes by
|
|
372
397
|
(Track, Satellite) and treated agreement within a group as
|
|
@@ -381,7 +406,6 @@ class PreflightGate:
|
|
|
381
406
|
check needs to catch. A second, independent bug in that version
|
|
382
407
|
also made the (Track, Satellite) grouping non-functional in
|
|
383
408
|
practice (wrong dict keys against the real scene object).
|
|
384
|
-
|
|
385
409
|
What this function guarantees regardless of which path fires:
|
|
386
410
|
every selected scene really is on a single, consistent track
|
|
387
411
|
(already enforced by select_consistent_geometry(); re-checked
|
|
@@ -401,7 +425,6 @@ class PreflightGate:
|
|
|
401
425
|
tracks.add(track)
|
|
402
426
|
|
|
403
427
|
issues: List[PreflightIssue] = []
|
|
404
|
-
|
|
405
428
|
if len(tracks) > 1:
|
|
406
429
|
# Should be unreachable if select_consistent_geometry() already
|
|
407
430
|
# ran correctly -- a real defensive check, not the expected path.
|
|
@@ -416,7 +439,6 @@ class PreflightGate:
|
|
|
416
439
|
))
|
|
417
440
|
|
|
418
441
|
satellites_present = geometry_report.get("satellites", set())
|
|
419
|
-
|
|
420
442
|
real_result = self._try_real_burst_family_check(selected) if self.attempt_real_burst_check else None
|
|
421
443
|
|
|
422
444
|
nodes_version = "unknown"
|
|
@@ -496,6 +518,7 @@ class PreflightGate:
|
|
|
496
518
|
f"specific pairs to correlate poorly too."
|
|
497
519
|
if internal_bridges else ""
|
|
498
520
|
)
|
|
521
|
+
|
|
499
522
|
issues.append(PreflightIssue(
|
|
500
523
|
code="BURST_FAMILY_DETECTED",
|
|
501
524
|
severity=SEVERITY_GREEN if resolved else SEVERITY_YELLOW,
|
|
@@ -531,6 +554,7 @@ class PreflightGate:
|
|
|
531
554
|
f"its own (see message). Full stack kept: {chosen_dates}."
|
|
532
555
|
),
|
|
533
556
|
))
|
|
557
|
+
|
|
534
558
|
# ACTUALLY apply the recommendation -- not just report it.
|
|
535
559
|
chosen_set = set(chosen_dates)
|
|
536
560
|
filtered_selected = [s for s in selected if str(s.datetime)[:10] in chosen_set]
|
|
@@ -561,7 +585,6 @@ class PreflightGate:
|
|
|
561
585
|
"absent."
|
|
562
586
|
),
|
|
563
587
|
))
|
|
564
|
-
|
|
565
588
|
return selected, issues
|
|
566
589
|
|
|
567
590
|
def _try_real_burst_family_check(
|
|
@@ -569,7 +592,6 @@ class PreflightGate:
|
|
|
569
592
|
) -> Optional[Tuple[List[str], Dict[str, Any]]]:
|
|
570
593
|
"""
|
|
571
594
|
Best-effort REAL pre-download burst-family classification.
|
|
572
|
-
|
|
573
595
|
Uses providers.copernicus_nodes to fetch only each candidate
|
|
574
596
|
date's lightweight annotation XML (not the full, multi-GB
|
|
575
597
|
product) plus its real orbit file, then runs the SAME, already-
|
|
@@ -580,7 +602,6 @@ class PreflightGate:
|
|
|
580
602
|
parse_slc_geometry() read only the annotation XML either way,
|
|
581
603
|
confirmed directly with an end-to-end test where the real,
|
|
582
604
|
unmodified parser reads a mini-zip built this way correctly.
|
|
583
|
-
|
|
584
605
|
Returns None -- never raises -- if anything real goes wrong:
|
|
585
606
|
missing "copernicus" credentials, a network failure, an
|
|
586
607
|
unexpected real API response shape, too few dates with both
|
|
@@ -589,7 +610,6 @@ class PreflightGate:
|
|
|
589
610
|
advisory," never "risk confirmed absent."
|
|
590
611
|
"""
|
|
591
612
|
import tempfile
|
|
592
|
-
|
|
593
613
|
try:
|
|
594
614
|
from pygeofetch.providers.copernicus_nodes import fetch_annotation_zip
|
|
595
615
|
from pygeofetch.core.orbits import fetch_orbit_file
|
|
@@ -615,16 +635,40 @@ class PreflightGate:
|
|
|
615
635
|
except Exception as exc:
|
|
616
636
|
logger.info("Real pre-download burst check: no annotation for %s (%s).", date, exc)
|
|
617
637
|
continue
|
|
638
|
+
|
|
618
639
|
try:
|
|
619
640
|
name = s.properties.get("name") if hasattr(s, "properties") else None
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
641
|
+
if not name:
|
|
642
|
+
continue
|
|
643
|
+
|
|
644
|
+
# ── FIX: ORBIT CASCADING ──
|
|
645
|
+
# Previously, this only tried "precise". If precise wasn't
|
|
646
|
+
# available (e.g., recent acquisitions), the date was dropped.
|
|
647
|
+
# Now it cascades: Precise -> Restituted -> Predicted.
|
|
648
|
+
orbit_file = None
|
|
649
|
+
for orbit_type in ["precise", "restituted", "predicted"]:
|
|
650
|
+
try:
|
|
651
|
+
orbit_file = fetch_orbit_file(
|
|
652
|
+
product_name=name, output_dir=str(work_dir), orbit_type=orbit_type,
|
|
653
|
+
)
|
|
654
|
+
if orbit_file is not None:
|
|
655
|
+
if orbit_type != "precise":
|
|
656
|
+
logger.warning(
|
|
657
|
+
"Orbit cascading: fell back to '%s' orbit for %s "
|
|
658
|
+
"(precise not available).", orbit_type, date
|
|
659
|
+
)
|
|
660
|
+
break
|
|
661
|
+
except Exception as exc:
|
|
662
|
+
logger.debug("Orbit cascade: '%s' failed for %s (%s).", orbit_type, date, exc)
|
|
663
|
+
continue
|
|
664
|
+
|
|
623
665
|
if orbit_file is None:
|
|
666
|
+
logger.info("Real pre-download burst check: no orbit file (any type) for %s.", date)
|
|
624
667
|
continue
|
|
668
|
+
|
|
625
669
|
orbit_files[date] = orbit_file
|
|
626
670
|
except Exception as exc:
|
|
627
|
-
logger.info("Real pre-download burst check:
|
|
671
|
+
logger.info("Real pre-download burst check: orbit cascade failed for %s (%s).", date, exc)
|
|
628
672
|
continue
|
|
629
673
|
|
|
630
674
|
usable_dates = sorted(set(safe_zips) & set(orbit_files))
|
|
@@ -640,7 +684,6 @@ class PreflightGate:
|
|
|
640
684
|
aoi_center_lat = (self.aoi_bbox.min_lat + self.aoi_bbox.max_lat) / 2
|
|
641
685
|
aoi_center_lon = (self.aoi_bbox.min_lon + self.aoi_bbox.max_lon) / 2
|
|
642
686
|
ground_point = geodetic_to_ecef(aoi_center_lat, aoi_center_lon, 0.0)
|
|
643
|
-
|
|
644
687
|
chosen_dates, family_report = select_burst_synchronized_dates(
|
|
645
688
|
usable_dates, safe_zips, orbit_files, ground_point,
|
|
646
689
|
min_majority_dates=self.min_majority_family_dates,
|
|
@@ -660,6 +703,22 @@ class PreflightGate:
|
|
|
660
703
|
self, selected: List[Any], search_report: Dict[str, Any],
|
|
661
704
|
issues: List[PreflightIssue],
|
|
662
705
|
) -> Dict[str, Any]:
|
|
706
|
+
# ── FIX: BANDWIDTH & DOWNLOAD MANIFEST ──
|
|
707
|
+
# Calculate estimated bandwidth and build a concrete manifest of
|
|
708
|
+
# what will actually be downloaded, preventing surprise disk fills.
|
|
709
|
+
ESTIMATED_GB_PER_SLC_SCENE = 7.5 # Sentinel-1 IW SLC average size
|
|
710
|
+
estimated_bandwidth_gb = len(selected) * ESTIMATED_GB_PER_SLC_SCENE
|
|
711
|
+
|
|
712
|
+
download_manifest = []
|
|
713
|
+
for s in selected:
|
|
714
|
+
props = s.properties if hasattr(s, "properties") else {}
|
|
715
|
+
download_manifest.append({
|
|
716
|
+
"date": str(s.datetime)[:10],
|
|
717
|
+
"scene_id": props.get("name"),
|
|
718
|
+
"satellite": props.get("platform"),
|
|
719
|
+
"polarisation": props.get("polarisation"),
|
|
720
|
+
})
|
|
721
|
+
|
|
663
722
|
return {
|
|
664
723
|
"search": {
|
|
665
724
|
"start_date": self.start_date,
|
|
@@ -681,6 +740,11 @@ class PreflightGate:
|
|
|
681
740
|
search_report.get("geometry_report", {}).get("satellites", set())
|
|
682
741
|
),
|
|
683
742
|
},
|
|
743
|
+
# NEW: Bandwidth & Manifest section
|
|
744
|
+
"bandwidth_and_manifest": {
|
|
745
|
+
"estimated_bandwidth_gb": round(estimated_bandwidth_gb, 2),
|
|
746
|
+
"download_manifest": download_manifest,
|
|
747
|
+
},
|
|
684
748
|
"preflight": {
|
|
685
749
|
"severity": self._overall_severity(issues),
|
|
686
750
|
"issues": [
|
|
@@ -356,6 +356,7 @@ class PhaseUnwrapper:
|
|
|
356
356
|
|
|
357
357
|
def _unwrap_cli(self, igram, corr, nlooks, min_conncomp_frac, min_region_size):
|
|
358
358
|
import numpy as np
|
|
359
|
+
import os
|
|
359
360
|
cli_path = _ensure_snaphu_cli()
|
|
360
361
|
height, width = igram.shape
|
|
361
362
|
bin_dir = Path(cli_path).parent
|
|
@@ -371,10 +372,6 @@ class PhaseUnwrapper:
|
|
|
371
372
|
corr.tofile(str(corr_path))
|
|
372
373
|
nlooks_int = max(1, int(nlooks))
|
|
373
374
|
|
|
374
|
-
# FIX: Removed 'CONNCOMPOUTFORMAT UINT_DATA'. It is not a valid
|
|
375
|
-
# SNAPHU parameter. SNAPHU always writes connected components as
|
|
376
|
-
# 32-bit unsigned integers by default. The strict Windows port
|
|
377
|
-
# we use rejects the config file if it sees unknown keys.
|
|
378
375
|
conf_text = f"""INFILE igram.cpx
|
|
379
376
|
INFILEFORMAT COMPLEX_DATA
|
|
380
377
|
CORRFILE corr.cor
|
|
@@ -396,7 +393,6 @@ NLOOKSAZ {nlooks_int}
|
|
|
396
393
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
397
394
|
result, unw_path, cc_path = _run_snaphu(tmp_dir)
|
|
398
395
|
|
|
399
|
-
# AUTO-REPAIR: Catch Windows STATUS_DLL_NOT_FOUND (0xC0000135)
|
|
400
396
|
if result.returncode in (3221225781, -1073741515):
|
|
401
397
|
logger.warning("SNAPHU crashed with STATUS_DLL_NOT_FOUND (0xC0000135). Auto-bundling runtime DLLs...")
|
|
402
398
|
self._ensure_runtime_dlls(bin_dir)
|
|
@@ -406,8 +402,44 @@ NLOOKSAZ {nlooks_int}
|
|
|
406
402
|
if result.returncode != 0:
|
|
407
403
|
raise RuntimeError(f"SNAPHU CLI failed with code {result.returncode}: {result.stderr}")
|
|
408
404
|
|
|
409
|
-
|
|
410
|
-
|
|
405
|
+
# ═══════════════════════════════════════════════════════════════
|
|
406
|
+
# SMART FILE READER (Handles Windows Port Quirks)
|
|
407
|
+
# ═══════════════════════════════════════════════════════════════
|
|
408
|
+
expected_pixels = height * width
|
|
409
|
+
|
|
410
|
+
# 1. Read Unwrapped Phase (Usually FLOAT_DATA = 4 bytes)
|
|
411
|
+
unw_size = os.path.getsize(str(unw_path))
|
|
412
|
+
unw_dtype = np.float32 if unw_size <= expected_pixels * 4 else np.float64
|
|
413
|
+
unw = np.fromfile(str(unw_path), dtype=unw_dtype)
|
|
414
|
+
|
|
415
|
+
# 2. Read Connected Components (Adapts to 1-byte, 2-byte, or 4-byte formats)
|
|
416
|
+
cc_size = os.path.getsize(str(cc_path))
|
|
417
|
+
if cc_size >= expected_pixels * 4:
|
|
418
|
+
cc_dtype = np.uint32
|
|
419
|
+
elif cc_size >= expected_pixels * 2:
|
|
420
|
+
cc_dtype = np.uint16
|
|
421
|
+
else:
|
|
422
|
+
cc_dtype = np.uint8 # The Windows port default
|
|
423
|
+
|
|
424
|
+
conncomp = np.fromfile(str(cc_path), dtype=cc_dtype)
|
|
425
|
+
|
|
426
|
+
# 3. Auto-heal the Windows "off-by-one" truncation bug
|
|
427
|
+
if conncomp.size < expected_pixels:
|
|
428
|
+
logger.warning(
|
|
429
|
+
"SNAPHU conncomp file is %d elements, expected %d. Padding missing bytes with 0 "
|
|
430
|
+
"(known Windows port truncation quirk).", conncomp.size, expected_pixels
|
|
431
|
+
)
|
|
432
|
+
conncomp = np.pad(conncomp, (0, expected_pixels - conncomp.size), mode='constant', constant_values=0)
|
|
433
|
+
elif conncomp.size > expected_pixels:
|
|
434
|
+
conncomp = conncomp[:expected_pixels]
|
|
435
|
+
|
|
436
|
+
if unw.size < expected_pixels:
|
|
437
|
+
unw = np.pad(unw, (0, expected_pixels - unw.size), mode='constant', constant_values=0)
|
|
438
|
+
elif unw.size > expected_pixels:
|
|
439
|
+
unw = unw[:expected_pixels]
|
|
440
|
+
|
|
441
|
+
unw = unw.reshape((height, width))
|
|
442
|
+
conncomp = conncomp.reshape((height, width))
|
|
411
443
|
|
|
412
444
|
total_pixels = height * width
|
|
413
445
|
labels, counts = np.unique(conncomp, return_counts=True)
|
|
@@ -591,4 +623,59 @@ NLOOKSAZ {nlooks_int}
|
|
|
591
623
|
|
|
592
624
|
def _np(self):
|
|
593
625
|
import numpy as np
|
|
594
|
-
return np
|
|
626
|
+
return np
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
import shutil
|
|
630
|
+
import subprocess
|
|
631
|
+
import logging
|
|
632
|
+
import sys
|
|
633
|
+
|
|
634
|
+
logger = logging.getLogger(__name__)
|
|
635
|
+
|
|
636
|
+
def verify_snaphu_ready():
|
|
637
|
+
"""
|
|
638
|
+
Proactively checks for snaphu availability to assure the user
|
|
639
|
+
that no manual installation is required.
|
|
640
|
+
"""
|
|
641
|
+
# 1. Check for the standard SNAPHU binary in the system PATH
|
|
642
|
+
snaphu_bin = shutil.which("snaphu")
|
|
643
|
+
if snaphu_bin:
|
|
644
|
+
# Optional: Verify the binary actually executes and isn't just a broken alias
|
|
645
|
+
try:
|
|
646
|
+
subprocess.run(
|
|
647
|
+
["snaphu"],
|
|
648
|
+
stdout=subprocess.DEVNULL,
|
|
649
|
+
stderr=subprocess.DEVNULL,
|
|
650
|
+
timeout=3
|
|
651
|
+
)
|
|
652
|
+
except Exception:
|
|
653
|
+
pass # Snaphu without args prints usage to stderr and exits; this is expected.
|
|
654
|
+
|
|
655
|
+
msg = f"✅ Success: snaphu binary detected at '{snaphu_bin}'. No manual installation needed."
|
|
656
|
+
print(msg)
|
|
657
|
+
logger.info(msg)
|
|
658
|
+
return True
|
|
659
|
+
|
|
660
|
+
# 2. Fallback: Check if it's available as a Python package/module
|
|
661
|
+
try:
|
|
662
|
+
import snaphu # or pysnaphu, depending on your wrapper
|
|
663
|
+
msg = "✅ Success: snaphu Python module detected. No manual installation needed."
|
|
664
|
+
print(msg)
|
|
665
|
+
logger.info(msg)
|
|
666
|
+
return True
|
|
667
|
+
except ImportError:
|
|
668
|
+
pass
|
|
669
|
+
|
|
670
|
+
# 3. If neither is found, handle the failure gracefully
|
|
671
|
+
msg = "❌ snaphu not found in PATH or Python environment. Phase unwrapping will fail."
|
|
672
|
+
print(msg)
|
|
673
|
+
logger.warning(msg)
|
|
674
|
+
return False
|
|
675
|
+
|
|
676
|
+
# --- Embed this early in your notebook or pipeline ---
|
|
677
|
+
snaphu_is_ready = verify_snaphu_ready()
|
|
678
|
+
|
|
679
|
+
if not snaphu_is_ready:
|
|
680
|
+
# Decide how to handle the missing dependency (see questions below)
|
|
681
|
+
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygeofetch
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -91,8 +91,8 @@ Requires-Dist: sidecar>=0.6.0; extra == "viz"
|
|
|
91
91
|
Requires-Dist: bokeh>=3.0.0; extra == "viz"
|
|
92
92
|
Requires-Dist: seaborn>=0.12.0; extra == "viz"
|
|
93
93
|
Requires-Dist: plotly>=5.14.0; extra == "viz"
|
|
94
|
-
Requires-Dist: whitebox>=2.
|
|
95
|
-
Requires-Dist: whiteboxgui>=2.
|
|
94
|
+
Requires-Dist: whitebox>=2.4.0; extra == "viz"
|
|
95
|
+
Requires-Dist: whiteboxgui>=2.4.0; extra == "viz"
|
|
96
96
|
Requires-Dist: geemap>=0.28.0; extra == "viz"
|
|
97
97
|
Requires-Dist: osmnx>=1.9.4; extra == "viz"
|
|
98
98
|
Requires-Dist: contextily>=1.9.4; extra == "viz"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pygeofetch"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.5.0"
|
|
8
8
|
description = "Universal satellite data pipeline + geospatial processing platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -94,8 +94,8 @@ viz = [
|
|
|
94
94
|
"bokeh>=3.0.0",
|
|
95
95
|
"seaborn>=0.12.0",
|
|
96
96
|
"plotly>=5.14.0",
|
|
97
|
-
"whitebox>=2.
|
|
98
|
-
"whiteboxgui>=2.
|
|
97
|
+
"whitebox>=2.4.0",
|
|
98
|
+
"whiteboxgui>=2.4.0",
|
|
99
99
|
"geemap>=0.28.0",
|
|
100
100
|
"osmnx>=1.9.4",
|
|
101
101
|
"contextily>=1.9.4",
|