pygeofetch 2.0.2__tar.gz → 2.0.4__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.0.2 → pygeofetch-2.0.4}/CHANGELOG.md +6 -6
- {pygeofetch-2.0.2/pygeofetch.egg-info → pygeofetch-2.0.4}/PKG-INFO +1 -1
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/__init__.py +2 -2
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/main.py +2 -2
- pygeofetch-2.0.4/pygeofetch/insar/esd.py +547 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/interferogram.py +137 -12
- pygeofetch-2.0.4/pygeofetch/insar/unwrap.py +735 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/meta.yaml +1 -1
- {pygeofetch-2.0.2 → pygeofetch-2.0.4/pygeofetch.egg-info}/PKG-INFO +1 -1
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pyproject.toml +1 -1
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_build_sbas_network.py +2 -2
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_coregister_upgrades.py +1 -1
- pygeofetch-2.0.2/pygeofetch/insar/esd.py +0 -361
- pygeofetch-2.0.2/pygeofetch/insar/unwrap.py +0 -626
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/LICENSE +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/MANIFEST.in +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/README.md +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/auth_commands.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/config_commands.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/download_commands.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/index_commands.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/pipeline_process_commands.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/postprocess_commands.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/preprocess_commands.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/sar_commands.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/search_commands.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/config/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/config/defaults.yaml +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/config/settings.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/authenticator.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/cache_manager.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/downloader.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/engine.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/logging.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/orbits.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/scheduler.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/searcher.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/annotation.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/atmosphere.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/coregister.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/deburst.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/extraction.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/flatearth.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/geolocation.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/gpu.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/ionosphere.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/splitspectrum_diagnostic.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/stack_selection.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/synthetic.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/timeseries.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/validate.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/visualize.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/workflow.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/models/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/models/download_task.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/models/satellite_data.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/models/search_query.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/models/user_auth.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/base.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/batch.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/indices.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/pipeline.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/postprocessor.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/preprocessor.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/sar.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/indices.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/landsat.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/loader.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/stacker.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/timeseries.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/airbus_oneatlas.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/aws_earth.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/base.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/copernicus.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/digitalglobe.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/earth_explorer_additional.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/element84.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/eodag_provider.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/esa_scihub.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/geoserver_generic.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/google_earth_engine.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/inpe_cbers.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/isro_bhuvan.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/jaxa_earth.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/maxar_gbdx.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/nasa_earthdata.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/noaa_big_data.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/opentopography.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/planet.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/planetary_computer.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/sentinel_hub.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/terrabotics.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/usgs.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/_native.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/_ost.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/_sarxarray.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/extraction.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/processor.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/file_utils.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/gcp_recovery.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/geo_utils.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/logging_setup.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/retry_handler.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/validators.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/viz/__init__.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/viz/map.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/viz/plot.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/SOURCES.txt +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/dependency_links.txt +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/entry_points.txt +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/not-zip-safe +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/requires.txt +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/top_level.txt +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/setup.cfg +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_atmospheric_circular_regression (1).py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_atmospheric_circular_regression.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_burst_metadata.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_burst_processing_integration.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_coregister_integration.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_era5_atmospheric_correction.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_extraction.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_flat_earth_phase.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_gcp_reprojection.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_insar.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_ionosphere.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_models.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_pgv_integration_contract.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_provider_geometry_audit (1).py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_provider_geometry_audit.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_provider_scene_disambiguation.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_providers.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_raster_collocation_integration.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_search_download.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_splitspectrum_diagnostic.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_utils.py +0 -0
- {pygeofetch-2.0.2 → pygeofetch-2.0.4}/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.0.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/2.0.4/).
|
|
5
5
|
|
|
6
|
-
## [2.0.
|
|
6
|
+
## [2.0.4] — 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.0.2/).
|
|
|
51
51
|
|
|
52
52
|
### Migration Guide
|
|
53
53
|
```python
|
|
54
|
-
# Before (v2.0.
|
|
54
|
+
# Before (v2.0.4)
|
|
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.0.
|
|
75
|
+
## [2.0.4] — 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.0.
|
|
192
|
-
| **2.0.
|
|
191
|
+
| **2.0.4** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
|
|
192
|
+
| **2.0.4** | 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 v2.0.
|
|
2
|
+
PyGeoFetch v2.0.4 — 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.0.
|
|
33
|
+
__version__ = "2.0.4"
|
|
34
34
|
__author__ = "PyGeoFetch Contributors"
|
|
35
35
|
__license__ = "MIT"
|
|
36
36
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
PyGeoFetch CLI v2.0.
|
|
2
|
+
PyGeoFetch CLI v2.0.4 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.0.
|
|
74
|
+
PyGeoFetch v2.0.4 — Universal Satellite Data Pipeline
|
|
75
75
|
===========================================================
|
|
76
76
|
Unified access to 22+ satellite data providers.
|
|
77
77
|
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real per-burst-overlap Enhanced Spectral Diversity (ESD), replacing the
|
|
3
|
+
previous whole-image approximation.
|
|
4
|
+
|
|
5
|
+
Formula confirmed against two independent, real academic sources:
|
|
6
|
+
Prats-Iraola et al. (2012), "Interferometric Processing of Sentinel-1
|
|
7
|
+
TOPS Data", IEEE TGRS.
|
|
8
|
+
Scheiber, R. & Moreira, A. (2000), "Coregistration of interferometric
|
|
9
|
+
SAR images using spectral diversity", IEEE TGRS.
|
|
10
|
+
|
|
11
|
+
The azimuth shift (in seconds) is:
|
|
12
|
+
delta_t = delta_phi / (2 * pi * delta_f_ovl)
|
|
13
|
+
where delta_phi is the double-difference phase between the forward-
|
|
14
|
+
and backward-looking interferograms in the overlap, and delta_f_ovl
|
|
15
|
+
is the Doppler centroid difference between the two looks (~1480 Hz
|
|
16
|
+
for IW mode).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from datetime import datetime, timedelta
|
|
22
|
+
import logging
|
|
23
|
+
from typing import List, Optional, Tuple
|
|
24
|
+
|
|
25
|
+
from pygeofetch.insar.annotation import SwathTiming
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("pygeofetch.insar.esd")
|
|
28
|
+
|
|
29
|
+
# Real Sentinel-1 IW Doppler centroid frequency difference between
|
|
30
|
+
# forward and backward looks in the burst overlap region.
|
|
31
|
+
# Confirmed: Prats-Iraola et al. (2012), Table I.
|
|
32
|
+
SENTINEL1_IW_DELTA_F_OVL_HZ = 1480.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def compute_overlap_row_ranges(
|
|
36
|
+
swath_timing: SwathTiming,
|
|
37
|
+
azimuth_time_interval_s: float,
|
|
38
|
+
) -> List[Tuple[Tuple[int, int], Tuple[int, int]]]:
|
|
39
|
+
"""
|
|
40
|
+
Compute the row ranges for each burst-overlap region.
|
|
41
|
+
|
|
42
|
+
Returns a list of ((bw_row_start, bw_row_end), (fw_row_start, fw_row_end))
|
|
43
|
+
tuples, one per adjacent burst pair. bw = backward-looking (end of
|
|
44
|
+
burst i), fw = forward-looking (start of burst i+1).
|
|
45
|
+
"""
|
|
46
|
+
overlaps = []
|
|
47
|
+
lines_per_burst = swath_timing.lines_per_burst
|
|
48
|
+
bursts = swath_timing.bursts
|
|
49
|
+
|
|
50
|
+
for i in range(len(bursts) - 1):
|
|
51
|
+
# Backward-looking: last rows of burst i
|
|
52
|
+
bw_row_end = (i + 1) * lines_per_burst - 1
|
|
53
|
+
bw_row_start = i * lines_per_burst
|
|
54
|
+
|
|
55
|
+
# Forward-looking: first rows of burst i+1
|
|
56
|
+
fw_row_start = (i + 1) * lines_per_burst
|
|
57
|
+
fw_row_end = (i + 2) * lines_per_burst - 1
|
|
58
|
+
|
|
59
|
+
# Compute overlap extent from timing
|
|
60
|
+
burst_i_end_time = (
|
|
61
|
+
bursts[i].azimuth_time.timestamp()
|
|
62
|
+
+ (lines_per_burst - 1) * azimuth_time_interval_s
|
|
63
|
+
)
|
|
64
|
+
burst_next_start_time = bursts[i + 1].azimuth_time.timestamp()
|
|
65
|
+
|
|
66
|
+
overlap_seconds = burst_i_end_time - burst_next_start_time
|
|
67
|
+
if overlap_seconds <= 0:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
overlap_lines = int(round(overlap_seconds / azimuth_time_interval_s))
|
|
71
|
+
if overlap_lines < 4:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Backward view: last `overlap_lines` rows of burst i
|
|
75
|
+
bw_row_start = bw_row_end - overlap_lines + 1
|
|
76
|
+
# Forward view: first `overlap_lines` rows of burst i+1
|
|
77
|
+
fw_row_end = fw_row_start + overlap_lines - 1
|
|
78
|
+
|
|
79
|
+
n_bw = bw_row_end - bw_row_start + 1
|
|
80
|
+
n_fw = fw_row_end - fw_row_start + 1
|
|
81
|
+
|
|
82
|
+
if n_bw != n_fw:
|
|
83
|
+
logger.warning(
|
|
84
|
+
"Overlap %d<->%d: backward-view row count (%d) does not "
|
|
85
|
+
"match forward-view row count (%d) -- unusual burst "
|
|
86
|
+
"timing; using the shorter of the two.",
|
|
87
|
+
i, i + 1, n_bw, n_fw,
|
|
88
|
+
)
|
|
89
|
+
n_common = min(n_bw, n_fw)
|
|
90
|
+
bw_row_start = bw_row_end - n_common + 1
|
|
91
|
+
fw_row_end = fw_row_start + n_common - 1
|
|
92
|
+
|
|
93
|
+
overlaps.append(((bw_row_start, bw_row_end), (fw_row_start, fw_row_end)))
|
|
94
|
+
|
|
95
|
+
return overlaps
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _windowed_coherence(
|
|
99
|
+
ref_imagette, sec_imagette, window: int = 7
|
|
100
|
+
) -> "np.ndarray":
|
|
101
|
+
"""
|
|
102
|
+
Compute windowed coherence map (standard InSAR coherence estimation).
|
|
103
|
+
|
|
104
|
+
Unlike the previous single-scalar approach, this returns a 2D array
|
|
105
|
+
of local coherence values, allowing us to:
|
|
106
|
+
1. Reject low-coherence pixels (water, vegetation, shadow)
|
|
107
|
+
2. Use only high-coherence pixels for the ESD phase estimate
|
|
108
|
+
3. Report a robust MEDIAN coherence for the overlap quality check
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
ref_imagette: Complex reference overlap region (n_rows x n_cols)
|
|
112
|
+
sec_imagette: Complex secondary overlap region (same shape)
|
|
113
|
+
window: Coherence estimation window size (default 7)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
2D float32 array of local coherence values [0, 1]
|
|
117
|
+
"""
|
|
118
|
+
import numpy as np
|
|
119
|
+
from scipy.ndimage import uniform_filter
|
|
120
|
+
|
|
121
|
+
cross = ref_imagette * np.conj(sec_imagette)
|
|
122
|
+
|
|
123
|
+
num_real = uniform_filter(cross.real, size=window)
|
|
124
|
+
num_imag = uniform_filter(cross.imag, size=window)
|
|
125
|
+
num = np.sqrt(num_real**2 + num_imag**2)
|
|
126
|
+
|
|
127
|
+
denom = np.sqrt(
|
|
128
|
+
uniform_filter(np.abs(ref_imagette) ** 2, size=window)
|
|
129
|
+
* uniform_filter(np.abs(sec_imagette) ** 2, size=window)
|
|
130
|
+
+ 1e-10
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
coherence = np.clip(num / denom, 0.0, 1.0).astype(np.float32)
|
|
134
|
+
return coherence
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _estimate_shift_from_overlap(
|
|
138
|
+
ref_bw, sec_bw, ref_fw, sec_fw,
|
|
139
|
+
delta_f_ovl_hz: float,
|
|
140
|
+
coherence_threshold: float = 0.3,
|
|
141
|
+
max_range_samples: int = 4096,
|
|
142
|
+
) -> Tuple[Optional[float], float]:
|
|
143
|
+
"""
|
|
144
|
+
Estimate azimuth shift from a single burst overlap using the
|
|
145
|
+
double-difference interferometric phase.
|
|
146
|
+
|
|
147
|
+
Memory optimization: subsamples in range if the overlap is wider
|
|
148
|
+
than max_range_samples (full scene = 23,674 cols; we only need
|
|
149
|
+
~4096 for a statistically robust estimate).
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
(shift_seconds, median_coherence)
|
|
153
|
+
"""
|
|
154
|
+
import numpy as np
|
|
155
|
+
|
|
156
|
+
n_rows_bw, n_cols = ref_bw.shape
|
|
157
|
+
n_rows_fw, _ = ref_fw.shape
|
|
158
|
+
|
|
159
|
+
# Range subsampling for memory efficiency on full scenes
|
|
160
|
+
if n_cols > max_range_samples:
|
|
161
|
+
step = n_cols // max_range_samples
|
|
162
|
+
col_idx = np.arange(0, n_cols, step)
|
|
163
|
+
ref_bw = ref_bw[:, col_idx]
|
|
164
|
+
sec_bw = sec_bw[:, col_idx]
|
|
165
|
+
ref_fw = ref_fw[:, col_idx]
|
|
166
|
+
sec_fw = sec_fw[:, col_idx]
|
|
167
|
+
n_cols = len(col_idx)
|
|
168
|
+
|
|
169
|
+
# Ensure matching row counts
|
|
170
|
+
n_common = min(n_rows_bw, n_rows_fw)
|
|
171
|
+
ref_bw = ref_bw[:n_common]
|
|
172
|
+
sec_bw = sec_bw[:n_common]
|
|
173
|
+
ref_fw = ref_fw[:n_common]
|
|
174
|
+
sec_fw = sec_fw[:n_common]
|
|
175
|
+
|
|
176
|
+
# Compute windowed coherence on the backward-looking interferogram
|
|
177
|
+
coh_map = _windowed_coherence(ref_bw, sec_bw, window=7)
|
|
178
|
+
median_coh = float(np.median(coh_map))
|
|
179
|
+
|
|
180
|
+
if median_coh < coherence_threshold:
|
|
181
|
+
return None, median_coh
|
|
182
|
+
|
|
183
|
+
# Form forward and backward interferograms
|
|
184
|
+
ifg_bw = ref_bw * np.conj(sec_bw) # backward-looking
|
|
185
|
+
ifg_fw = ref_fw * np.conj(sec_fw) # forward-looking
|
|
186
|
+
|
|
187
|
+
# Double-difference phase: the key ESD quantity
|
|
188
|
+
# delta_phi = angle(ifg_bw) - angle(ifg_fw)
|
|
189
|
+
# Only use pixels where local coherence > threshold
|
|
190
|
+
mask = coh_map > coherence_threshold
|
|
191
|
+
|
|
192
|
+
if mask.sum() < 100:
|
|
193
|
+
return None, median_coh
|
|
194
|
+
|
|
195
|
+
# Compute double-difference phase only at high-coherence pixels
|
|
196
|
+
phase_bw = np.angle(ifg_bw)
|
|
197
|
+
phase_fw = np.angle(ifg_fw)
|
|
198
|
+
delta_phase = phase_bw[mask] - phase_fw[mask]
|
|
199
|
+
|
|
200
|
+
# Robust phase estimate: circular mean (not arithmetic mean)
|
|
201
|
+
mean_delta_phi = np.angle(np.mean(np.exp(1j * delta_phase)))
|
|
202
|
+
|
|
203
|
+
# Convert to time shift
|
|
204
|
+
shift_seconds = mean_delta_phi / (2.0 * np.pi * delta_f_ovl_hz)
|
|
205
|
+
|
|
206
|
+
return float(shift_seconds), median_coh
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
from typing import Dict, List, Optional, Tuple
|
|
210
|
+
|
|
211
|
+
def overlap_time_windows(
|
|
212
|
+
swath_timing: SwathTiming, azimuth_time_interval_s: float
|
|
213
|
+
) -> List[Tuple[int, "datetime", "datetime"]]:
|
|
214
|
+
"""
|
|
215
|
+
Absolute azimuth-time window of every adjacent-burst overlap -- the
|
|
216
|
+
ground area imaged by BOTH burst i (its trailing lines) and burst
|
|
217
|
+
i+1 (its leading lines) in THIS scene.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List of (overlap_index, window_start, window_end) as absolute
|
|
221
|
+
azimuth datetimes.
|
|
222
|
+
"""
|
|
223
|
+
bursts = swath_timing.bursts
|
|
224
|
+
lines_per_burst = swath_timing.lines_per_burst
|
|
225
|
+
out = []
|
|
226
|
+
for i in range(len(bursts) - 1):
|
|
227
|
+
start = bursts[i + 1].azimuth_time
|
|
228
|
+
end = bursts[i].azimuth_time + timedelta(
|
|
229
|
+
seconds=(lines_per_burst - 1) * azimuth_time_interval_s
|
|
230
|
+
)
|
|
231
|
+
out.append((i, start, end))
|
|
232
|
+
return out
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def compute_common_ground_overlaps(
|
|
237
|
+
ref_timing: SwathTiming,
|
|
238
|
+
sec_timing: SwathTiming,
|
|
239
|
+
azimuth_time_interval_s: float,
|
|
240
|
+
) -> List[Dict]:
|
|
241
|
+
"""
|
|
242
|
+
Per-reference-overlap diagnostic of how much COMMON double-covered
|
|
243
|
+
ground (in azimuth lines) this date pair actually shares.
|
|
244
|
+
|
|
245
|
+
For each reference overlap window, intersect it with every secondary
|
|
246
|
+
overlap window and keep the largest intersection. That intersection
|
|
247
|
+
length is the only ground on which the ESD double-difference is
|
|
248
|
+
physically meaningful for this pair.
|
|
249
|
+
|
|
250
|
+
This is the number that cleanly separates the two regimes:
|
|
251
|
+
- same-family pairs (identical burst timing): common lines ~= the
|
|
252
|
+
full overlap length (tens of lines per overlap);
|
|
253
|
+
- cross-family pairs (8 vs 9 bursts, shifted timing): common lines
|
|
254
|
+
shrink toward 0, which is exactly why ESD finds few/no usable
|
|
255
|
+
overlaps for them -- a physical limitation, not a bug.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
List of dicts, one per reference overlap, each with:
|
|
259
|
+
ref_overlap_index, ref_window, best_sec_overlap_index,
|
|
260
|
+
sec_window, common_lines, common_window
|
|
261
|
+
"""
|
|
262
|
+
dt = azimuth_time_interval_s
|
|
263
|
+
ref_wins = overlap_time_windows(ref_timing, dt)
|
|
264
|
+
sec_wins = overlap_time_windows(sec_timing, dt)
|
|
265
|
+
|
|
266
|
+
report = []
|
|
267
|
+
for i, r_start, r_end in ref_wins:
|
|
268
|
+
best_j, best_lines, best_win = None, 0, None
|
|
269
|
+
for j, s_start, s_end in sec_wins:
|
|
270
|
+
t0, t1 = max(r_start, s_start), min(r_end, s_end)
|
|
271
|
+
if t1 <= t0:
|
|
272
|
+
continue # no intersection for this secondary overlap
|
|
273
|
+
n_lines = int(round((t1 - t0).total_seconds() / dt)) + 1
|
|
274
|
+
if n_lines > best_lines:
|
|
275
|
+
best_j, best_lines, best_win = j, n_lines, (t0, t1)
|
|
276
|
+
report.append({
|
|
277
|
+
"ref_overlap_index": i,
|
|
278
|
+
"ref_window": (r_start, r_end),
|
|
279
|
+
"best_sec_overlap_index": best_j,
|
|
280
|
+
"sec_window": best_win,
|
|
281
|
+
"common_lines": best_lines,
|
|
282
|
+
"common_window": best_win,
|
|
283
|
+
})
|
|
284
|
+
return report
|
|
285
|
+
|
|
286
|
+
def estimate_esd_shift_per_burst_overlap(
|
|
287
|
+
ref_complex, sec_complex,
|
|
288
|
+
swath_timing, azimuth_time_interval_s,
|
|
289
|
+
row_offset: int = 0,
|
|
290
|
+
sec_swath_timing=None, # NEW: secondary burst timing
|
|
291
|
+
delta_f_ovl_hz=SENTINEL1_IW_DELTA_F_OVL_HZ,
|
|
292
|
+
coherence_threshold: float = 0.2,
|
|
293
|
+
min_common_lines: int = 3,
|
|
294
|
+
):
|
|
295
|
+
import numpy as np
|
|
296
|
+
from scipy.ndimage import uniform_filter
|
|
297
|
+
|
|
298
|
+
dt = azimuth_time_interval_s
|
|
299
|
+
L = swath_timing.lines_per_burst
|
|
300
|
+
ref_wins = overlap_time_windows(swath_timing, dt)
|
|
301
|
+
# If no secondary timing given, fall back to ref-only (old behaviour)
|
|
302
|
+
sec_wins = (overlap_time_windows(sec_swath_timing, dt)
|
|
303
|
+
if sec_swath_timing is not None else ref_wins)
|
|
304
|
+
|
|
305
|
+
per_overlap_shifts = []
|
|
306
|
+
skip_reasons = []
|
|
307
|
+
|
|
308
|
+
for (i, r_start, r_end) in ref_wins:
|
|
309
|
+
# Find the secondary overlap with the largest time intersection
|
|
310
|
+
best = None
|
|
311
|
+
for (j, s_start, s_end) in sec_wins:
|
|
312
|
+
t0, t1 = max(r_start, s_start), min(r_end, s_end)
|
|
313
|
+
n_lines = int((t1 - t0).total_seconds() / dt) + 1
|
|
314
|
+
if best is None or n_lines > best[0]:
|
|
315
|
+
best = (n_lines, t0, t1)
|
|
316
|
+
n_lines, t0, t1 = best
|
|
317
|
+
|
|
318
|
+
if n_lines < min_common_lines:
|
|
319
|
+
per_overlap_shifts.append(None)
|
|
320
|
+
skip_reasons.append("no_common_ground")
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
# Map the common time window to reference rows (bw=burst i tail,
|
|
324
|
+
# fw=burst i+1 head). Secondary is coregistered to the ref grid,
|
|
325
|
+
# so it is indexed at the same rows.
|
|
326
|
+
bw_rows, fw_rows = [], []
|
|
327
|
+
for k in range(n_lines):
|
|
328
|
+
tt = t0 + timedelta(seconds=k * dt)
|
|
329
|
+
bw_rows.append(i * L + round((tt - swath_timing.bursts[i].azimuth_time).total_seconds() / dt) - row_offset)
|
|
330
|
+
fw_rows.append((i + 1) * L + round((tt - swath_timing.bursts[i + 1].azimuth_time).total_seconds() / dt) - row_offset)
|
|
331
|
+
|
|
332
|
+
bw = np.array([r for r in bw_rows if 0 <= r < ref_complex.shape[0]])
|
|
333
|
+
fw = np.array([r for r in fw_rows if 0 <= r < ref_complex.shape[0]])
|
|
334
|
+
if len(bw) < min_common_lines or len(fw) < min_common_lines:
|
|
335
|
+
per_overlap_shifts.append(None)
|
|
336
|
+
skip_reasons.append("outside_array_bounds")
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
ref_bw, sec_bw = ref_complex[bw], sec_complex[bw]
|
|
340
|
+
ref_fw, sec_fw = ref_complex[fw], sec_complex[fw]
|
|
341
|
+
igram_bw = ref_bw * np.conj(sec_bw)
|
|
342
|
+
igram_fw = ref_fw * np.conj(sec_fw)
|
|
343
|
+
|
|
344
|
+
# Windowed coherence gate (same as before)
|
|
345
|
+
n_common = len(bw)
|
|
346
|
+
coh_window = min(5, n_common) if n_common >= 3 else 1
|
|
347
|
+
if coh_window >= 3:
|
|
348
|
+
num = np.abs(uniform_filter(igram_bw.real, size=coh_window)
|
|
349
|
+
+ 1j * uniform_filter(igram_bw.imag, size=coh_window))
|
|
350
|
+
denom = np.sqrt(uniform_filter(np.abs(ref_bw)**2, size=coh_window)
|
|
351
|
+
* uniform_filter(np.abs(sec_bw)**2, size=coh_window) + 1e-10)
|
|
352
|
+
valid = np.isfinite(num / denom) & ((num / denom) >= coherence_threshold)
|
|
353
|
+
else:
|
|
354
|
+
valid = np.ones(igram_bw.shape, dtype=bool)
|
|
355
|
+
|
|
356
|
+
if valid.mean() < 0.5:
|
|
357
|
+
per_overlap_shifts.append(None)
|
|
358
|
+
skip_reasons.append(f"low_coherence({100*valid.mean():.0f}%)")
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
double_diff = igram_fw * np.conj(igram_bw)
|
|
362
|
+
mean_phase = np.angle(np.mean(double_diff[valid]))
|
|
363
|
+
per_overlap_shifts.append(float(mean_phase / (2 * np.pi * delta_f_ovl_hz)))
|
|
364
|
+
skip_reasons.append("used")
|
|
365
|
+
|
|
366
|
+
valid_shifts = [s for s in per_overlap_shifts if s is not None]
|
|
367
|
+
if not valid_shifts:
|
|
368
|
+
n_outside = skip_reasons.count("outside_array_bounds")
|
|
369
|
+
n_nocommon = skip_reasons.count("no_common_ground")
|
|
370
|
+
n_low = sum(1 for r in skip_reasons if r.startswith("low_coherence"))
|
|
371
|
+
logger.warning(
|
|
372
|
+
"ESD: no usable overlaps of %d (%d no common ground, %d outside, %d low coh)",
|
|
373
|
+
len(ref_wins), n_nocommon, n_outside, n_low)
|
|
374
|
+
return None, per_overlap_shifts
|
|
375
|
+
|
|
376
|
+
combined = float(np.median(valid_shifts))
|
|
377
|
+
logger.info("ESD common-ground: %d/%d usable, shift=%.6f ms (%.4f px)",
|
|
378
|
+
len(valid_shifts), len(ref_wins), combined*1000, combined/dt)
|
|
379
|
+
return combined, per_overlap_shifts
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# def estimate_esd_shift_per_burst_overlap(
|
|
383
|
+
# ref_complex,
|
|
384
|
+
# sec_complex,
|
|
385
|
+
# swath_timing: SwathTiming,
|
|
386
|
+
# azimuth_time_interval_s: float,
|
|
387
|
+
# row_offset: int = 0,
|
|
388
|
+
# delta_f_ovl_hz: float = SENTINEL1_IW_DELTA_F_OVL_HZ,
|
|
389
|
+
# coherence_threshold: float = 0.3,
|
|
390
|
+
# ) -> Tuple[Optional[float], List[Optional[float]]]:
|
|
391
|
+
# """
|
|
392
|
+
# Real per-burst-overlap ESD: estimate azimuth misregistration from
|
|
393
|
+
# the actual burst overlap regions, replacing the previous whole-image
|
|
394
|
+
# single-shift approximation.
|
|
395
|
+
|
|
396
|
+
# Uses WINDOWED coherence estimation (7x7 window) and MEDIAN coherence
|
|
397
|
+
# for the quality check, matching SNAP's approach. This is critical for
|
|
398
|
+
# full-scene processing where the overlap spans the entire range width
|
|
399
|
+
# (~23,000 pixels) including ocean, vegetation, and other low-coherence
|
|
400
|
+
# terrain that would drag a global-mean coherence below threshold.
|
|
401
|
+
|
|
402
|
+
# Memory optimization: subsamples in range to max 4096 columns per
|
|
403
|
+
# overlap, reducing peak memory from ~600 MB to ~100 MB per overlap
|
|
404
|
+
# while maintaining statistical robustness.
|
|
405
|
+
|
|
406
|
+
# Args:
|
|
407
|
+
# ref_complex, sec_complex: Real, coregistered (at least coarsely)
|
|
408
|
+
# complex SLC arrays, same shape -- full burst stack, or an
|
|
409
|
+
# already-cropped extract (see row_offset).
|
|
410
|
+
# swath_timing: Real burst metadata from annotation.parse_burst_info().
|
|
411
|
+
# azimuth_time_interval_s: Real per-line time spacing.
|
|
412
|
+
# row_offset: Real full-scene row that ref_complex/sec_complex's
|
|
413
|
+
# row 0 corresponds to. Default 0 assumes the arrays start at
|
|
414
|
+
# the full-scene row 0.
|
|
415
|
+
# delta_f_ovl_hz: Doppler centroid difference in the overlap.
|
|
416
|
+
# coherence_threshold: Minimum MEDIAN windowed coherence for an
|
|
417
|
+
# overlap to be considered usable.
|
|
418
|
+
|
|
419
|
+
# Returns:
|
|
420
|
+
# (shift_seconds, per_overlap_shifts): shift_seconds is the real,
|
|
421
|
+
# median azimuth timing shift (seconds) across all valid burst
|
|
422
|
+
# overlaps (None if no overlap gave a usable estimate);
|
|
423
|
+
# per_overlap_shifts is one entry per adjacent burst pair (None
|
|
424
|
+
# where that overlap's coherence was too low or fell entirely
|
|
425
|
+
# outside the given arrays).
|
|
426
|
+
# """
|
|
427
|
+
# import numpy as np
|
|
428
|
+
|
|
429
|
+
# if not isinstance(swath_timing, SwathTiming):
|
|
430
|
+
# raise TypeError(
|
|
431
|
+
# f"estimate_esd_shift_per_burst_overlap expects a SwathTiming "
|
|
432
|
+
# f"object, got {type(swath_timing).__name__}. Pass the result of "
|
|
433
|
+
# f"parse_burst_info(), not a raw bursts list."
|
|
434
|
+
# )
|
|
435
|
+
|
|
436
|
+
# overlaps = compute_overlap_row_ranges(swath_timing, azimuth_time_interval_s)
|
|
437
|
+
# per_overlap_shifts: List[Optional[float]] = []
|
|
438
|
+
|
|
439
|
+
# n_outside = 0
|
|
440
|
+
# n_below_coh = 0
|
|
441
|
+
# n_usable = 0
|
|
442
|
+
|
|
443
|
+
# h, w = ref_complex.shape
|
|
444
|
+
|
|
445
|
+
# for i, ((bw_start, bw_end), (fw_start, fw_end)) in enumerate(overlaps):
|
|
446
|
+
# # Map full-scene rows to local array rows
|
|
447
|
+
# local_bw_start = bw_start - row_offset
|
|
448
|
+
# local_bw_end = bw_end - row_offset
|
|
449
|
+
# local_fw_start = fw_start - row_offset
|
|
450
|
+
# local_fw_end = fw_end - row_offset
|
|
451
|
+
|
|
452
|
+
# # Check bounds
|
|
453
|
+
# if (local_bw_start < 0 or local_bw_end >= h
|
|
454
|
+
# or local_fw_start < 0 or local_fw_end >= h):
|
|
455
|
+
# n_outside += 1
|
|
456
|
+
# per_overlap_shifts.append(None)
|
|
457
|
+
# logger.debug(
|
|
458
|
+
# "Overlap %d<->%d: outside array bounds "
|
|
459
|
+
# "(local rows bw=[%d,%d], fw=[%d,%d], array height=%d)",
|
|
460
|
+
# i, i + 1, local_bw_start, local_bw_end,
|
|
461
|
+
# local_fw_start, local_fw_end, h,
|
|
462
|
+
# )
|
|
463
|
+
# continue
|
|
464
|
+
|
|
465
|
+
# # Extract overlap regions
|
|
466
|
+
# ref_bw = ref_complex[local_bw_start:local_bw_end + 1, :]
|
|
467
|
+
# sec_bw = sec_complex[local_bw_start:local_bw_end + 1, :]
|
|
468
|
+
# ref_fw = ref_complex[local_fw_start:local_fw_end + 1, :]
|
|
469
|
+
# sec_fw = sec_complex[local_fw_start:local_fw_end + 1, :]
|
|
470
|
+
|
|
471
|
+
# # Estimate shift with windowed coherence
|
|
472
|
+
# shift_s, median_coh = _estimate_shift_from_overlap(
|
|
473
|
+
# ref_bw, sec_bw, ref_fw, sec_fw,
|
|
474
|
+
# delta_f_ovl_hz=delta_f_ovl_hz,
|
|
475
|
+
# coherence_threshold=coherence_threshold,
|
|
476
|
+
# )
|
|
477
|
+
|
|
478
|
+
# if shift_s is None:
|
|
479
|
+
# n_below_coh += 1
|
|
480
|
+
# per_overlap_shifts.append(None)
|
|
481
|
+
# logger.debug(
|
|
482
|
+
# "Overlap %d<->%d: median windowed coherence %.3f < threshold %.2f",
|
|
483
|
+
# i, i + 1, median_coh, coherence_threshold,
|
|
484
|
+
# )
|
|
485
|
+
# else:
|
|
486
|
+
# n_usable += 1
|
|
487
|
+
# per_overlap_shifts.append(shift_s)
|
|
488
|
+
# logger.debug(
|
|
489
|
+
# "Overlap %d<->%d: shift=%.6f ms, median_coh=%.3f",
|
|
490
|
+
# i, i + 1, shift_s * 1000, median_coh,
|
|
491
|
+
# )
|
|
492
|
+
|
|
493
|
+
# # Report diagnostics
|
|
494
|
+
# if n_usable == 0:
|
|
495
|
+
# if n_outside > 0 and n_below_coh > 0:
|
|
496
|
+
# logger.warning(
|
|
497
|
+
# "Real per-burst-overlap ESD found no usable burst overlaps "
|
|
498
|
+
# "out of %d total: %d fell outside the given (possibly "
|
|
499
|
+
# "cropped) arrays, %d were below coherence_threshold=%.2f. "
|
|
500
|
+
# "A mix of both -- see the per-overlap detail at DEBUG level.",
|
|
501
|
+
# len(overlaps), n_outside, n_below_coh, coherence_threshold,
|
|
502
|
+
# )
|
|
503
|
+
# elif n_outside > 0:
|
|
504
|
+
# logger.warning(
|
|
505
|
+
# "Real per-burst-overlap ESD found no usable burst overlaps "
|
|
506
|
+
# "out of %d total: all %d fell outside the given (possibly "
|
|
507
|
+
# "cropped) arrays.",
|
|
508
|
+
# len(overlaps), n_outside,
|
|
509
|
+
# )
|
|
510
|
+
# else:
|
|
511
|
+
# logger.warning(
|
|
512
|
+
# "Real per-burst-overlap ESD found no usable burst overlaps "
|
|
513
|
+
# "out of %d total: 0 fell outside the given (possibly "
|
|
514
|
+
# "cropped) arrays, %d were below coherence_threshold=%.2f. "
|
|
515
|
+
# "Every real, in-bounds overlap was tested and found "
|
|
516
|
+
# "genuinely below the coherence threshold -- this reflects "
|
|
517
|
+
# "real decorrelation in the data, not something a larger "
|
|
518
|
+
# "AOI would fix.",
|
|
519
|
+
# len(overlaps), n_below_coh, coherence_threshold,
|
|
520
|
+
# )
|
|
521
|
+
# return None, per_overlap_shifts
|
|
522
|
+
|
|
523
|
+
# # Combine valid shifts via median (robust to outliers)
|
|
524
|
+
# valid_shifts = [s for s in per_overlap_shifts if s is not None]
|
|
525
|
+
# combined_shift = float(np.median(valid_shifts))
|
|
526
|
+
|
|
527
|
+
# logger.info(
|
|
528
|
+
# "Real per-burst-overlap ESD: %d/%d overlaps usable, "
|
|
529
|
+
# "combined shift=%.6f ms (%.4f px)",
|
|
530
|
+
# n_usable, len(overlaps),
|
|
531
|
+
# combined_shift * 1000,
|
|
532
|
+
# combined_shift / azimuth_time_interval_s,
|
|
533
|
+
# )
|
|
534
|
+
|
|
535
|
+
# return combined_shift, per_overlap_shifts
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
# def overlap_time_windows(swath_timing, dt):
|
|
539
|
+
# """Absolute azimuth-time windows for each adjacent-burst overlap."""
|
|
540
|
+
# bursts = swath_timing.bursts
|
|
541
|
+
# L = swath_timing.lines_per_burst
|
|
542
|
+
# out = []
|
|
543
|
+
# for i in range(len(bursts) - 1):
|
|
544
|
+
# start = bursts[i + 1].azimuth_time
|
|
545
|
+
# end = bursts[i].azimuth_time + timedelta(seconds=(L - 1) * dt)
|
|
546
|
+
# out.append((i, start, end))
|
|
547
|
+
# return out
|