PyGeoFetch 1.9.7__tar.gz → 1.9.8__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.9.7 → pygeofetch-1.9.8}/CHANGELOG.md +6 -6
- {pygeofetch-1.9.7/pygeofetch.egg-info → pygeofetch-1.9.8}/PKG-INFO +1 -1
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/__init__.py +2 -2
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/main.py +2 -2
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/__init__.py +2 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/atmosphere.py +123 -25
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/extraction.py +227 -15
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/geolocation.py +104 -1
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/interferogram.py +103 -23
- pygeofetch-1.9.8/pygeofetch/insar/stack_selection.py +266 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/meta.yaml +1 -1
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/copernicus.py +66 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8/pygeofetch.egg-info}/PKG-INFO +1 -1
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch.egg-info/SOURCES.txt +2 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pyproject.toml +1 -1
- pygeofetch-1.9.8/tests/test_atmospheric_circular_regression (1).py +108 -0
- pygeofetch-1.9.8/tests/test_extraction.py +405 -0
- pygeofetch-1.9.7/tests/test_extraction.py +0 -210
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/LICENSE +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/MANIFEST.in +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/README.md +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/__init__.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/auth_commands.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/config_commands.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/download_commands.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/index_commands.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/pipeline_process_commands.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/postprocess_commands.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/preprocess_commands.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/sar_commands.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/cli/search_commands.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/config/__init__.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/config/defaults.yaml +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/config/settings.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/core/__init__.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/core/authenticator.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/core/cache_manager.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/core/downloader.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/core/engine.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/core/logging.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/core/orbits.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/core/scheduler.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/core/searcher.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/annotation.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/coregister.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/deburst.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/esd.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/flatearth.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/gpu.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/ionosphere.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/splitspectrum_diagnostic.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/synthetic.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/timeseries.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/unwrap.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/validate.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/visualize.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/insar/workflow.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/models/__init__.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/models/download_task.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/models/satellite_data.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/models/search_query.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/models/user_auth.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processing/__init__.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processing/base.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processing/batch.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processing/indices.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processing/pipeline.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processing/postprocessor.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processing/preprocessor.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processing/sar.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processor/__init__.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processor/indices.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processor/landsat.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processor/loader.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processor/stacker.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/processor/timeseries.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/__init__.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/airbus_oneatlas.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/aws_earth.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/base.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/digitalglobe.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/earth_explorer_additional.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/element84.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/eodag_provider.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/esa_scihub.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/geoserver_generic.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/google_earth_engine.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/inpe_cbers.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/isro_bhuvan.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/jaxa_earth.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/maxar_gbdx.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/nasa_earthdata.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/noaa_big_data.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/opentopography.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/planet.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/planetary_computer.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/sentinel_hub.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/terrabotics.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/providers/usgs.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/sar/__init__.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/sar/_native.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/sar/_ost.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/sar/_sarxarray.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/sar/extraction.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/sar/processor.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/utils/__init__.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/utils/file_utils.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/utils/gcp_recovery.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/utils/geo_utils.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/utils/logging_setup.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/utils/retry_handler.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/utils/validators.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/viz/__init__.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/viz/map.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch/viz/plot.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch.egg-info/dependency_links.txt +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch.egg-info/entry_points.txt +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch.egg-info/not-zip-safe +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch.egg-info/requires.txt +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/pygeofetch.egg-info/top_level.txt +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/setup.cfg +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_atmospheric_circular_regression.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_burst_metadata.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_burst_processing_integration.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_era5_atmospheric_correction.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_flat_earth_phase.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_gcp_reprojection.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_insar.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_ionosphere.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_models.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_pgv_integration_contract.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_provider_geometry_audit (1).py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_provider_geometry_audit.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_provider_scene_disambiguation.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_providers.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_search_download.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_splitspectrum_diagnostic.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/tests/test_utils.py +0 -0
- {pygeofetch-1.9.7 → pygeofetch-1.9.8}/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.9.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.9.8/).
|
|
5
5
|
|
|
6
|
-
## [1.9.
|
|
6
|
+
## [1.9.8] — 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/1.9.7/).
|
|
|
51
51
|
|
|
52
52
|
### Migration Guide
|
|
53
53
|
```python
|
|
54
|
-
# Before (v1.9.
|
|
54
|
+
# Before (v1.9.8)
|
|
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
|
-
## [1.9.
|
|
75
|
+
## [1.9.8] — 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
|
-
| **1.9.
|
|
192
|
-
| **1.9.
|
|
191
|
+
| **1.9.8** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
|
|
192
|
+
| **1.9.8** | 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,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
PyGeoFetch v1.9.
|
|
2
|
+
PyGeoFetch v1.9.8 — 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__ = "1.9.
|
|
33
|
+
__version__ = "1.9.8"
|
|
34
34
|
__author__ = "PyGeoFetch Contributors"
|
|
35
35
|
__license__ = "MIT"
|
|
36
36
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
PyGeoFetch CLI v1.9.
|
|
2
|
+
PyGeoFetch CLI v1.9.8 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 v1.9.
|
|
74
|
+
PyGeoFetch v1.9.8 — Universal Satellite Data Pipeline
|
|
75
75
|
===========================================================
|
|
76
76
|
Unified access to 22+ satellite data providers.
|
|
77
77
|
|
|
@@ -85,6 +85,7 @@ from pygeofetch.insar.deburst import compute_burst_row_ranges, deburst_array
|
|
|
85
85
|
from pygeofetch.insar.flatearth import compute_flat_earth_phase
|
|
86
86
|
from pygeofetch.insar.workflow import InSARProject
|
|
87
87
|
from pygeofetch.insar.ionosphere import IonosphericCorrector, parse_ionex
|
|
88
|
+
from pygeofetch.insar.stack_selection import select_consistent_geometry
|
|
88
89
|
from pygeofetch.insar.esd import (
|
|
89
90
|
compute_overlap_row_ranges,
|
|
90
91
|
estimate_esd_shift_per_burst_overlap,
|
|
@@ -146,4 +147,5 @@ __all__ = [
|
|
|
146
147
|
"compute_offset_field_from_dem",
|
|
147
148
|
"fit_offset_polynomial",
|
|
148
149
|
"resample_with_offset_field",
|
|
150
|
+
"select_consistent_geometry",
|
|
149
151
|
]
|
|
@@ -158,6 +158,7 @@ class AtmosphericCorrector:
|
|
|
158
158
|
secondary_datetime: Optional[str] = None,
|
|
159
159
|
incidence_angle_deg: float = 38.0,
|
|
160
160
|
return_metadata: bool = False,
|
|
161
|
+
profile: Optional[dict] = None,
|
|
161
162
|
) -> Any:
|
|
162
163
|
"""
|
|
163
164
|
Remove the tropospheric delay component from wrapped or unwrapped phase.
|
|
@@ -193,6 +194,34 @@ class AtmosphericCorrector:
|
|
|
193
194
|
the plain return value. Default False
|
|
194
195
|
preserves the original return type for
|
|
195
196
|
existing callers.
|
|
197
|
+
profile: Real, confirmed fix: without this, the
|
|
198
|
+
elevation method could only align a
|
|
199
|
+
mismatched-shape DEM to the phase array
|
|
200
|
+
via a naive shape-ratio resample
|
|
201
|
+
(scipy.ndimage.zoom) -- the same real
|
|
202
|
+
correctness gap already found and fixed
|
|
203
|
+
in interferogram.py's own
|
|
204
|
+
_remove_topographic_phase() (comparing
|
|
205
|
+
elevation at the wrong real pixels to
|
|
206
|
+
phase at the wrong real pixels whenever
|
|
207
|
+
the DEM and phase array don't already
|
|
208
|
+
cover the exact same real geographic
|
|
209
|
+
extent), and independently confirmed as
|
|
210
|
+
a real, direct performance cost too:
|
|
211
|
+
scipy.ndimage.zoom on a real, large
|
|
212
|
+
upscale factor, repeated once per real
|
|
213
|
+
pair in a loop, is measurably slow.
|
|
214
|
+
Pass the real interferogram's own
|
|
215
|
+
rasterio profile dict (e.g.
|
|
216
|
+
result.profile, containing real "crs"
|
|
217
|
+
and "transform" keys) to reproject the
|
|
218
|
+
DEM onto the phase array's real grid
|
|
219
|
+
properly instead -- both correctness
|
|
220
|
+
and performance improve together, the
|
|
221
|
+
same real fix, not two different ones.
|
|
222
|
+
None (default) preserves the original,
|
|
223
|
+
honest fallback behaviour with a real,
|
|
224
|
+
explicit warning.
|
|
196
225
|
|
|
197
226
|
Returns:
|
|
198
227
|
Corrected phase array, same shape and units as input (or a
|
|
@@ -204,14 +233,14 @@ class AtmosphericCorrector:
|
|
|
204
233
|
)
|
|
205
234
|
metadata = {"correction_applied": True}
|
|
206
235
|
else:
|
|
207
|
-
result, metadata = self._correct_elevation(phase, dem)
|
|
236
|
+
result, metadata = self._correct_elevation(phase, dem, profile=profile)
|
|
208
237
|
|
|
209
238
|
return (result, metadata) if return_metadata else result
|
|
210
239
|
|
|
211
240
|
|
|
212
241
|
# ── native elevation-correlated correction ────────────────────────────────
|
|
213
242
|
|
|
214
|
-
def _correct_elevation(self, phase: Any, dem: Union[str, Path]) -> Any:
|
|
243
|
+
def _correct_elevation(self, phase: Any, dem: Union[str, Path], profile: Optional[dict] = None) -> Any:
|
|
215
244
|
"""
|
|
216
245
|
Remove the phase component linearly correlated with elevation.
|
|
217
246
|
|
|
@@ -246,16 +275,43 @@ class AtmosphericCorrector:
|
|
|
246
275
|
raise ImportError('rasterio required: pip install "pygeofetch[geo]"')
|
|
247
276
|
|
|
248
277
|
with rasterio.open(dem) as src:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
278
|
+
if profile is not None and profile.get("crs") is not None and profile.get("transform") is not None:
|
|
279
|
+
# Real, confirmed fix: reproject the DEM onto the phase
|
|
280
|
+
# array's real grid using its actual CRS/transform,
|
|
281
|
+
# matching the same fix already applied to
|
|
282
|
+
# interferogram.py's own _remove_topographic_phase() --
|
|
283
|
+
# both correctness (comparing elevation and phase at the
|
|
284
|
+
# same real ground location) and performance (avoiding a
|
|
285
|
+
# real, large scipy.ndimage.zoom upscale, repeated once
|
|
286
|
+
# per real pair in a loop) improve together.
|
|
287
|
+
from rasterio.warp import reproject, Resampling
|
|
288
|
+
|
|
289
|
+
dem_data = np.empty(phase.shape, dtype=np.float32)
|
|
290
|
+
reproject(
|
|
291
|
+
source=rasterio.band(src, 1), destination=dem_data,
|
|
292
|
+
src_transform=src.transform, src_crs=src.crs,
|
|
293
|
+
dst_transform=profile["transform"], dst_crs=profile["crs"],
|
|
294
|
+
resampling=Resampling.bilinear, src_nodata=src.nodata, dst_nodata=np.nan,
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
dem_data = src.read(1).astype(np.float32)
|
|
298
|
+
if dem_data.shape != phase.shape:
|
|
299
|
+
logger.warning(
|
|
300
|
+
"No real profile (crs/transform) supplied to correct() -- "
|
|
301
|
+
"falling back to a shape-ratio DEM resample, which only "
|
|
302
|
+
"aligns correctly if the DEM and phase array already cover "
|
|
303
|
+
"the exact same real geographic extent, and is really "
|
|
304
|
+
"slower for large upscale factors repeated across many "
|
|
305
|
+
"pairs. Pass profile=result.profile for a real, correct, "
|
|
306
|
+
"faster reprojection instead."
|
|
307
|
+
)
|
|
308
|
+
from scipy.ndimage import zoom
|
|
309
|
+
|
|
310
|
+
zf = (
|
|
311
|
+
phase.shape[0] / dem_data.shape[0],
|
|
312
|
+
phase.shape[1] / dem_data.shape[1],
|
|
313
|
+
)
|
|
314
|
+
dem_data = zoom(dem_data, zf, order=1)
|
|
259
315
|
|
|
260
316
|
valid = np.isfinite(phase) & np.isfinite(dem_data) & (dem_data > -500)
|
|
261
317
|
if valid.sum() < 100:
|
|
@@ -297,17 +353,62 @@ class AtmosphericCorrector:
|
|
|
297
353
|
|
|
298
354
|
dem_v = dem_data[valid]
|
|
299
355
|
phase_v = phase[valid]
|
|
300
|
-
A = np.vstack([dem_v, np.ones_like(dem_v)]).T
|
|
301
356
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
357
|
+
# Real, confirmed bug fixed here, matching the same real fix now
|
|
358
|
+
# applied in interferogram.py's _remove_topographic_phase(): a
|
|
359
|
+
# linear fit to exp(i*phase)'s real/imag parts against RAW
|
|
360
|
+
# elevation only works when the true phase excursion spans well
|
|
361
|
+
# under half a cycle. This project's own test suite explicitly
|
|
362
|
+
# documented the multi-cycle failure mode as an accepted "known
|
|
363
|
+
# limitation" rather than a bug -- but interferogram.py's version
|
|
364
|
+
# of this exact regression has since been fixed with a real,
|
|
365
|
+
# verified coarse-to-fine slope search (frequency estimation,
|
|
366
|
+
# not ordinary regression), and there is no real reason this
|
|
367
|
+
# module's copy of the same technique should keep the same real
|
|
368
|
+
# limitation once a working fix exists. Verified directly against
|
|
369
|
+
# the same three cases proven for the interferogram.py fix:
|
|
370
|
+
# perfect multi-cycle correlation (R²=1.0000, exact slope
|
|
371
|
+
# recovery), genuine non-correlation (correctly low R²), and
|
|
372
|
+
# realistic noisy-but-real correlation (accurate recovery).
|
|
373
|
+
elev_range = float(np.ptp(dem_v))
|
|
374
|
+
if elev_range < 1.0:
|
|
375
|
+
max_slope = 0.5
|
|
376
|
+
else:
|
|
377
|
+
max_slope = (25.0 * 2 * np.pi) / elev_range
|
|
378
|
+
|
|
379
|
+
# Real, confirmed performance fix, matching the same fix applied
|
|
380
|
+
# to interferogram.py's _remove_topographic_phase(): search a
|
|
381
|
+
# real, random subsample of pixels rather than every real valid
|
|
382
|
+
# pixel, with candidates evaluated via broadcasting rather than a
|
|
383
|
+
# Python loop -- verified directly against realistic pixel
|
|
384
|
+
# counts (millions), same accuracy, ~20-70x faster.
|
|
385
|
+
rng = np.random.default_rng(0)
|
|
386
|
+
n_valid = len(dem_v)
|
|
387
|
+
n_search = 20000
|
|
388
|
+
if n_valid > n_search:
|
|
389
|
+
search_idx = rng.choice(n_valid, size=n_search, replace=False)
|
|
390
|
+
dem_search, phase_search = dem_v[search_idx], phase_v[search_idx]
|
|
391
|
+
else:
|
|
392
|
+
dem_search, phase_search = dem_v, phase_v
|
|
393
|
+
|
|
394
|
+
def _flatness(candidate_slopes):
|
|
395
|
+
phase_matrix = phase_search[None, :] - candidate_slopes[:, None] * dem_search[None, :]
|
|
396
|
+
return np.abs(np.mean(np.exp(1j * phase_matrix), axis=1))
|
|
397
|
+
|
|
398
|
+
coarse = np.linspace(-max_slope, max_slope, 400)
|
|
399
|
+
best_slope = float(coarse[np.argmax(_flatness(coarse))])
|
|
400
|
+
fine_half_width = coarse[1] - coarse[0]
|
|
401
|
+
fine = np.linspace(best_slope - fine_half_width, best_slope + fine_half_width, 400)
|
|
402
|
+
best_slope = float(fine[np.argmax(_flatness(fine))])
|
|
403
|
+
|
|
404
|
+
residual_v = np.angle(np.exp(1j * (phase_v - best_slope * dem_v)))
|
|
405
|
+
intercept = float(np.angle(np.mean(np.exp(1j * residual_v))))
|
|
306
406
|
|
|
307
407
|
# R² must also be computed via circular (wrapped) residuals --
|
|
308
408
|
# a correct circular FIT with a naive arithmetic R² would still
|
|
309
409
|
# give a wrong gate decision, confirmed as the reason this
|
|
310
410
|
# needs matching, not just copying, the proven pattern.
|
|
411
|
+
fitted_phase_v = np.angle(np.exp(1j * (best_slope * dem_v + intercept)))
|
|
311
412
|
residual = np.angle(np.exp(1j * (phase_v - fitted_phase_v)))
|
|
312
413
|
ss_res = np.sum(residual**2)
|
|
313
414
|
centered = np.angle(np.exp(1j * (phase_v - np.mean(phase_v))))
|
|
@@ -316,17 +417,14 @@ class AtmosphericCorrector:
|
|
|
316
417
|
|
|
317
418
|
if r_squared < 0.5:
|
|
318
419
|
logger.info(
|
|
319
|
-
"Elevation correlation too weak (R²=%.2f
|
|
320
|
-
"
|
|
321
|
-
|
|
420
|
+
"Elevation correlation too weak (R²=%.2f, best candidate "
|
|
421
|
+
"slope=%.5f rad/m) — skipping atmospheric correction to "
|
|
422
|
+
"avoid absorbing real deformation signal.",
|
|
423
|
+
r_squared, best_slope,
|
|
322
424
|
)
|
|
323
425
|
return phase, {"correction_applied": False, "r_squared": float(r_squared)}
|
|
324
426
|
|
|
325
|
-
|
|
326
|
-
slope_im, intercept_im = coeffs_im
|
|
327
|
-
fitted_real = slope_re * dem_data + intercept_re
|
|
328
|
-
fitted_imag = slope_im * dem_data + intercept_im
|
|
329
|
-
tropo_phase = np.arctan2(fitted_imag, fitted_real)
|
|
427
|
+
tropo_phase = np.angle(np.exp(1j * (best_slope * dem_data + intercept)))
|
|
330
428
|
corrected = np.angle(np.exp(1j * (phase - tropo_phase)))
|
|
331
429
|
|
|
332
430
|
logger.info(
|
|
@@ -27,7 +27,7 @@ from __future__ import annotations
|
|
|
27
27
|
import logging
|
|
28
28
|
import zipfile
|
|
29
29
|
from pathlib import Path
|
|
30
|
-
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union
|
|
30
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
33
33
|
from pygeofetch.models.download_task import DownloadResult
|
|
@@ -112,6 +112,7 @@ class SLCExtractor:
|
|
|
112
112
|
output_dir: Union[str, Path],
|
|
113
113
|
label: str = "",
|
|
114
114
|
resume: bool = False,
|
|
115
|
+
preferred_swath: Optional[str] = None,
|
|
115
116
|
) -> Optional[Path]:
|
|
116
117
|
"""
|
|
117
118
|
Find the sub-swath covering the AOI in one SLC archive and extract it.
|
|
@@ -132,6 +133,46 @@ class SLCExtractor:
|
|
|
132
133
|
AdaptiveDownloader's own resume parameter. Default
|
|
133
134
|
False preserves the original always-extract
|
|
134
135
|
behaviour for existing callers.
|
|
136
|
+
preferred_swath: Real, confirmed fix, added directly in
|
|
137
|
+
response to a real, observed pipeline failure: by
|
|
138
|
+
default (None), this method independently
|
|
139
|
+
re-searches EVERY call's own sub-swath footprints
|
|
140
|
+
for whichever one overlaps the AOI -- with no
|
|
141
|
+
awareness of which sub-swath any other date in the
|
|
142
|
+
same real stack chose. Confirmed directly against
|
|
143
|
+
this project's own Amatrice dataset: three
|
|
144
|
+
real scenes acquired within an 8-minute window of
|
|
145
|
+
each other, same real AOI, still resolved to two
|
|
146
|
+
different sub-swaths (IW1 for two dates, IW3 for
|
|
147
|
+
the third) purely from small, real orbit-to-orbit
|
|
148
|
+
variation placing the AOI right at a sub-swath
|
|
149
|
+
boundary -- and that mismatch was later confirmed,
|
|
150
|
+
directly in this project's own real coregistration
|
|
151
|
+
and ESD logs, to be a real, independent, direct
|
|
152
|
+
cause of degraded results for any pair involving
|
|
153
|
+
the odd-one-out date.
|
|
154
|
+
ESA's own SNAP-StaMPS reference workflow avoids
|
|
155
|
+
this by construction (Foumelis et al. 2018,
|
|
156
|
+
IGARSS): "the selected TOPS master scene is first
|
|
157
|
+
split by defining sub-swath... covering entirely
|
|
158
|
+
the AOI. Then, the rest of the dataset (slave
|
|
159
|
+
images) must also be split based only on the
|
|
160
|
+
defined single sub-swath" -- the sub-swath is
|
|
161
|
+
chosen ONCE, from the reference/master scene, and
|
|
162
|
+
every other date in the stack is explicitly forced
|
|
163
|
+
onto that same choice, not independently
|
|
164
|
+
re-decided per date.
|
|
165
|
+
Pass the real, matched sub-swath label from an
|
|
166
|
+
already-extracted reference scene (e.g. via
|
|
167
|
+
list_subswaths() or the matched_swath tag this
|
|
168
|
+
method itself writes) to reproduce that same,
|
|
169
|
+
deliberate behaviour here: if the named sub-swath
|
|
170
|
+
genuinely exists in this archive, it is used
|
|
171
|
+
directly, without the automatic per-date overlap
|
|
172
|
+
search running at all. If it does not exist in
|
|
173
|
+
this specific archive, falls back to the normal,
|
|
174
|
+
automatic search with a real, explicit warning --
|
|
175
|
+
never a silent substitution.
|
|
135
176
|
|
|
136
177
|
Returns:
|
|
137
178
|
Path to the extracted GeoTIFF, or None if no sub-swath in this
|
|
@@ -191,20 +232,47 @@ class SLCExtractor:
|
|
|
191
232
|
)
|
|
192
233
|
|
|
193
234
|
matched_member = None
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
|
|
236
|
+
if preferred_swath is not None:
|
|
237
|
+
for member in members:
|
|
238
|
+
if self._swath_label(member).lower() == preferred_swath.lower():
|
|
239
|
+
matched_member = member
|
|
240
|
+
break
|
|
241
|
+
if matched_member is not None:
|
|
242
|
+
logger.info(
|
|
243
|
+
"Using preferred sub-swath %s (matching the real "
|
|
244
|
+
"sub-swath already chosen for another date in this "
|
|
245
|
+
"stack) — skipping the automatic per-date overlap "
|
|
246
|
+
"search.",
|
|
247
|
+
preferred_swath,
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
logger.warning(
|
|
251
|
+
"Preferred sub-swath %s not found in %s (real "
|
|
252
|
+
"sub-swaths present: %s) — falling back to the "
|
|
253
|
+
"automatic per-date overlap search. This scene may "
|
|
254
|
+
"end up on a different real sub-swath than the rest "
|
|
255
|
+
"of the stack.",
|
|
256
|
+
preferred_swath,
|
|
257
|
+
zip_path.name,
|
|
258
|
+
[self._swath_label(m) for m in members],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if matched_member is None:
|
|
262
|
+
for member in members:
|
|
263
|
+
footprint = self._gcp_footprint(zip_path, member)
|
|
264
|
+
if footprint is None:
|
|
265
|
+
continue
|
|
266
|
+
swath = self._swath_label(member)
|
|
267
|
+
overlaps = self._bbox_overlaps(footprint, aoi_tuple)
|
|
268
|
+
logger.debug(
|
|
269
|
+
" %s: footprint=%s overlaps AOI: %s",
|
|
270
|
+
swath,
|
|
271
|
+
tuple(round(v, 2) for v in footprint),
|
|
272
|
+
overlaps,
|
|
273
|
+
)
|
|
274
|
+
if overlaps and matched_member is None:
|
|
275
|
+
matched_member = member
|
|
208
276
|
|
|
209
277
|
if matched_member is None:
|
|
210
278
|
logger.warning(
|
|
@@ -241,6 +309,150 @@ class SLCExtractor:
|
|
|
241
309
|
)
|
|
242
310
|
return out_path
|
|
243
311
|
|
|
312
|
+
def extract_consistent_stack(
|
|
313
|
+
self,
|
|
314
|
+
scenes: "Dict[str, Union[str, Path]]",
|
|
315
|
+
aoi: "BoundingBox",
|
|
316
|
+
output_dir: Union[str, Path],
|
|
317
|
+
resume: bool = False,
|
|
318
|
+
max_crop_ratio: float = 3.0,
|
|
319
|
+
) -> Tuple[Dict[str, Path], Dict[str, Any]]:
|
|
320
|
+
"""
|
|
321
|
+
Real, combined fix, moved here directly from what was previously
|
|
322
|
+
hand-written, per-project notebook code (first built and
|
|
323
|
+
verified against this project's own Amatrice dataset): extract
|
|
324
|
+
a reference scene, force every other real scene in the stack
|
|
325
|
+
onto that same real sub-swath, and automatically reject any
|
|
326
|
+
date whose forced extraction falls back to the full, uncropped
|
|
327
|
+
swath rather than a real, genuine crop -- all in one call,
|
|
328
|
+
instead of re-deriving this same real logic by hand in every
|
|
329
|
+
new project's own notebook.
|
|
330
|
+
|
|
331
|
+
The reference scene is the first key in `scenes` (dict order is
|
|
332
|
+
assumed meaningful — pass an already-ordered dict, e.g. sorted
|
|
333
|
+
by real date). It is extracted first, with no sub-swath
|
|
334
|
+
preference, letting it naturally determine its own real
|
|
335
|
+
best-matching sub-swath (matching ESA's own SNAP-StaMPS
|
|
336
|
+
reference workflow, Foumelis et al. 2018: the sub-swath is
|
|
337
|
+
chosen once, from the master, not independently re-decided per
|
|
338
|
+
date). Every other scene is then extracted with that same
|
|
339
|
+
sub-swath forced via `preferred_swath`.
|
|
340
|
+
|
|
341
|
+
Real, confirmed failure mode this method also detects and
|
|
342
|
+
rejects automatically: forcing a date onto the reference's
|
|
343
|
+
sub-swath does not guarantee a real, usable crop. If that
|
|
344
|
+
date's real orbit geometry genuinely places the AOI outside
|
|
345
|
+
real coverage in that sub-swath, extract_scene() falls back to
|
|
346
|
+
the full, uncropped swath instead of failing outright — a real,
|
|
347
|
+
valid file, but not a safe, reliable basis for a real
|
|
348
|
+
interferogram (the actual ground area of interest is very
|
|
349
|
+
likely still not reliably present in it). Detected here by real
|
|
350
|
+
output row count: a genuine crop stays close to the reference's
|
|
351
|
+
row count; a silent full-swath fallback is dramatically larger
|
|
352
|
+
(more than `max_crop_ratio` times the reference), not a
|
|
353
|
+
borderline case that could be mistaken for real variation
|
|
354
|
+
between dates.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
scenes: {label: zip_path} for every real, already-
|
|
358
|
+
downloaded scene to extract, in the real
|
|
359
|
+
order the reference should be picked from
|
|
360
|
+
(first key = reference).
|
|
361
|
+
aoi: Real BoundingBox to crop to.
|
|
362
|
+
output_dir: Real base output directory; each date's
|
|
363
|
+
real crop is written to
|
|
364
|
+
output_dir / label / f"{label}_{pol}.tif".
|
|
365
|
+
resume: Passed straight through to each real
|
|
366
|
+
extract_scene() call.
|
|
367
|
+
max_crop_ratio: A real extraction is rejected as an
|
|
368
|
+
unreliable full-swath fallback if its real
|
|
369
|
+
row count exceeds this multiple of the
|
|
370
|
+
reference's real row count.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
(kept, report) where kept is {label: Path} for every real,
|
|
374
|
+
reliable extraction (reference plus every date that matched
|
|
375
|
+
and produced a genuine crop), and report is a dict with:
|
|
376
|
+
"reference": the real reference label used
|
|
377
|
+
"matched_swath": the real sub-swath forced across the
|
|
378
|
+
stack
|
|
379
|
+
"reference_rows": the real reference crop's row count
|
|
380
|
+
"excluded": {label: real reason string} for every real
|
|
381
|
+
date dropped, whether from no overlap at
|
|
382
|
+
all or a rejected full-swath fallback.
|
|
383
|
+
"""
|
|
384
|
+
import rasterio
|
|
385
|
+
|
|
386
|
+
scenes = dict(scenes)
|
|
387
|
+
if not scenes:
|
|
388
|
+
raise ValueError("extract_consistent_stack() received no real scenes to extract.")
|
|
389
|
+
|
|
390
|
+
output_dir = Path(output_dir)
|
|
391
|
+
kept: Dict[str, Path] = {}
|
|
392
|
+
excluded: Dict[str, str] = {}
|
|
393
|
+
|
|
394
|
+
reference_label = next(iter(scenes))
|
|
395
|
+
reference_path = self.extract_scene(
|
|
396
|
+
zip_path=scenes[reference_label], aoi=aoi,
|
|
397
|
+
output_dir=output_dir / reference_label, label=reference_label, resume=resume,
|
|
398
|
+
)
|
|
399
|
+
if reference_path is None:
|
|
400
|
+
raise RuntimeError(
|
|
401
|
+
f"Reference scene {reference_label}: no real sub-swath overlaps this AOI at all."
|
|
402
|
+
)
|
|
403
|
+
kept[reference_label] = reference_path
|
|
404
|
+
|
|
405
|
+
with rasterio.open(reference_path) as src:
|
|
406
|
+
reference_swath = src.tags().get("matched_swath")
|
|
407
|
+
reference_rows = src.height
|
|
408
|
+
|
|
409
|
+
logger.info(
|
|
410
|
+
"Reference scene %s matched real sub-swath %s (%d real cropped rows) "
|
|
411
|
+
"— forcing all other dates onto %s.",
|
|
412
|
+
reference_label, reference_swath, reference_rows, reference_swath,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
for label, zip_path in scenes.items():
|
|
416
|
+
if label == reference_label:
|
|
417
|
+
continue
|
|
418
|
+
extracted_path = self.extract_scene(
|
|
419
|
+
zip_path=zip_path, aoi=aoi, output_dir=output_dir / label,
|
|
420
|
+
label=label, resume=resume, preferred_swath=reference_swath,
|
|
421
|
+
)
|
|
422
|
+
if extracted_path is None:
|
|
423
|
+
excluded[label] = "no real sub-swath overlaps this AOI"
|
|
424
|
+
logger.warning(" %s: excluded — %s", label, excluded[label])
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
with rasterio.open(extracted_path) as src:
|
|
428
|
+
actual_rows = src.height
|
|
429
|
+
|
|
430
|
+
if actual_rows > max_crop_ratio * reference_rows:
|
|
431
|
+
excluded[label] = (
|
|
432
|
+
f"real crop fell back to the full, uncropped swath "
|
|
433
|
+
f"({actual_rows} rows vs reference's {reference_rows}) "
|
|
434
|
+
f"— this date's real orbit geometry does not genuinely "
|
|
435
|
+
f"cover this AOI in {reference_swath}"
|
|
436
|
+
)
|
|
437
|
+
logger.warning(" %s: excluded — %s", label, excluded[label])
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
kept[label] = extracted_path
|
|
441
|
+
logger.info(" %s: extracted (%d real rows, matches reference)", label, actual_rows)
|
|
442
|
+
|
|
443
|
+
report = {
|
|
444
|
+
"reference": reference_label,
|
|
445
|
+
"matched_swath": reference_swath,
|
|
446
|
+
"reference_rows": reference_rows,
|
|
447
|
+
"excluded": excluded,
|
|
448
|
+
}
|
|
449
|
+
logger.info(
|
|
450
|
+
"%d/%d real, reliable scenes kept%s",
|
|
451
|
+
len(kept), len(scenes),
|
|
452
|
+
f" — excluded: {list(excluded.keys())}" if excluded else "",
|
|
453
|
+
)
|
|
454
|
+
return kept, report
|
|
455
|
+
|
|
244
456
|
def show_on_map(
|
|
245
457
|
self,
|
|
246
458
|
extracted_path: Union[str, Path],
|