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.
Files changed (146) hide show
  1. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/CHANGELOG.md +6 -6
  2. {pygeofetch-2.0.2/pygeofetch.egg-info → pygeofetch-2.0.4}/PKG-INFO +1 -1
  3. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/__init__.py +2 -2
  4. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/main.py +2 -2
  5. pygeofetch-2.0.4/pygeofetch/insar/esd.py +547 -0
  6. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/interferogram.py +137 -12
  7. pygeofetch-2.0.4/pygeofetch/insar/unwrap.py +735 -0
  8. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/meta.yaml +1 -1
  9. {pygeofetch-2.0.2 → pygeofetch-2.0.4/pygeofetch.egg-info}/PKG-INFO +1 -1
  10. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pyproject.toml +1 -1
  11. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_build_sbas_network.py +2 -2
  12. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_coregister_upgrades.py +1 -1
  13. pygeofetch-2.0.2/pygeofetch/insar/esd.py +0 -361
  14. pygeofetch-2.0.2/pygeofetch/insar/unwrap.py +0 -626
  15. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/LICENSE +0 -0
  16. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/MANIFEST.in +0 -0
  17. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/README.md +0 -0
  18. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/__init__.py +0 -0
  19. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/auth_commands.py +0 -0
  20. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/config_commands.py +0 -0
  21. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/download_commands.py +0 -0
  22. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/index_commands.py +0 -0
  23. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/pipeline_process_commands.py +0 -0
  24. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/postprocess_commands.py +0 -0
  25. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/preprocess_commands.py +0 -0
  26. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/sar_commands.py +0 -0
  27. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/cli/search_commands.py +0 -0
  28. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/config/__init__.py +0 -0
  29. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/config/defaults.yaml +0 -0
  30. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/config/settings.py +0 -0
  31. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/__init__.py +0 -0
  32. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/authenticator.py +0 -0
  33. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/cache_manager.py +0 -0
  34. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/downloader.py +0 -0
  35. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/engine.py +0 -0
  36. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/logging.py +0 -0
  37. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/orbits.py +0 -0
  38. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/scheduler.py +0 -0
  39. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/core/searcher.py +0 -0
  40. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/__init__.py +0 -0
  41. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/annotation.py +0 -0
  42. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/atmosphere.py +0 -0
  43. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/coregister.py +0 -0
  44. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/deburst.py +0 -0
  45. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/extraction.py +0 -0
  46. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/flatearth.py +0 -0
  47. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/geolocation.py +0 -0
  48. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/gpu.py +0 -0
  49. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/ionosphere.py +0 -0
  50. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/splitspectrum_diagnostic.py +0 -0
  51. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/stack_selection.py +0 -0
  52. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/synthetic.py +0 -0
  53. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/timeseries.py +0 -0
  54. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/validate.py +0 -0
  55. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/visualize.py +0 -0
  56. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/insar/workflow.py +0 -0
  57. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/models/__init__.py +0 -0
  58. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/models/download_task.py +0 -0
  59. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/models/satellite_data.py +0 -0
  60. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/models/search_query.py +0 -0
  61. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/models/user_auth.py +0 -0
  62. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/__init__.py +0 -0
  63. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/base.py +0 -0
  64. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/batch.py +0 -0
  65. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/indices.py +0 -0
  66. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/pipeline.py +0 -0
  67. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/postprocessor.py +0 -0
  68. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/preprocessor.py +0 -0
  69. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processing/sar.py +0 -0
  70. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/__init__.py +0 -0
  71. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/indices.py +0 -0
  72. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/landsat.py +0 -0
  73. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/loader.py +0 -0
  74. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/stacker.py +0 -0
  75. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/processor/timeseries.py +0 -0
  76. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/__init__.py +0 -0
  77. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/airbus_oneatlas.py +0 -0
  78. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
  79. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/aws_earth.py +0 -0
  80. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/base.py +0 -0
  81. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/copernicus.py +0 -0
  82. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/digitalglobe.py +0 -0
  83. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/earth_explorer_additional.py +0 -0
  84. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/element84.py +0 -0
  85. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/eodag_provider.py +0 -0
  86. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/esa_scihub.py +0 -0
  87. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/geoserver_generic.py +0 -0
  88. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/google_earth_engine.py +0 -0
  89. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/inpe_cbers.py +0 -0
  90. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/isro_bhuvan.py +0 -0
  91. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/jaxa_earth.py +0 -0
  92. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/maxar_gbdx.py +0 -0
  93. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/nasa_earthdata.py +0 -0
  94. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
  95. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/noaa_big_data.py +0 -0
  96. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/opentopography.py +0 -0
  97. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/planet.py +0 -0
  98. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/planetary_computer.py +0 -0
  99. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/sentinel_hub.py +0 -0
  100. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/terrabotics.py +0 -0
  101. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/providers/usgs.py +0 -0
  102. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/__init__.py +0 -0
  103. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/_native.py +0 -0
  104. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/_ost.py +0 -0
  105. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/_sarxarray.py +0 -0
  106. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/extraction.py +0 -0
  107. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/sar/processor.py +0 -0
  108. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/__init__.py +0 -0
  109. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/file_utils.py +0 -0
  110. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/gcp_recovery.py +0 -0
  111. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/geo_utils.py +0 -0
  112. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/logging_setup.py +0 -0
  113. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/retry_handler.py +0 -0
  114. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/utils/validators.py +0 -0
  115. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/viz/__init__.py +0 -0
  116. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/viz/map.py +0 -0
  117. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch/viz/plot.py +0 -0
  118. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/SOURCES.txt +0 -0
  119. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/dependency_links.txt +0 -0
  120. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/entry_points.txt +0 -0
  121. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/not-zip-safe +0 -0
  122. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/requires.txt +0 -0
  123. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/pygeofetch.egg-info/top_level.txt +0 -0
  124. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/setup.cfg +0 -0
  125. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_atmospheric_circular_regression (1).py +0 -0
  126. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_atmospheric_circular_regression.py +0 -0
  127. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_burst_metadata.py +0 -0
  128. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_burst_processing_integration.py +0 -0
  129. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_coregister_integration.py +0 -0
  130. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_era5_atmospheric_correction.py +0 -0
  131. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_extraction.py +0 -0
  132. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_flat_earth_phase.py +0 -0
  133. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_gcp_reprojection.py +0 -0
  134. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_insar.py +0 -0
  135. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_ionosphere.py +0 -0
  136. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_models.py +0 -0
  137. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_pgv_integration_contract.py +0 -0
  138. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_provider_geometry_audit (1).py +0 -0
  139. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_provider_geometry_audit.py +0 -0
  140. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_provider_scene_disambiguation.py +0 -0
  141. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_providers.py +0 -0
  142. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_raster_collocation_integration.py +0 -0
  143. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_search_download.py +0 -0
  144. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_splitspectrum_diagnostic.py +0 -0
  145. {pygeofetch-2.0.2 → pygeofetch-2.0.4}/tests/test_utils.py +0 -0
  146. {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.2/).
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/2.0.4/).
5
5
 
6
- ## [2.0.2] — 2026-07-29
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.2)
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.2] — 2026-07-12
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.2** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
192
- | **2.0.2** | 2026-07-12 | 22 providers, 17 indices, Sentinel-1C/D |
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygeofetch
3
- Version: 2.0.2
3
+ Version: 2.0.4
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
@@ -1,5 +1,5 @@
1
1
  """
2
- PyGeoFetch v2.0.2 — Universal Satellite Data Pipeline.
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.2"
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 main entry point.
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.2 — Universal Satellite Data Pipeline
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