PyGeoFetch 1.9.6__tar.gz → 1.9.7__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 (138) hide show
  1. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/CHANGELOG.md +6 -6
  2. {pygeofetch-1.9.6/pygeofetch.egg-info → pygeofetch-1.9.7}/PKG-INFO +1 -1
  3. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/__init__.py +2 -2
  4. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/main.py +2 -2
  5. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/esd.py +45 -3
  6. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/flatearth.py +34 -1
  7. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/interferogram.py +143 -9
  8. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/meta.yaml +1 -1
  9. {pygeofetch-1.9.6 → pygeofetch-1.9.7/pygeofetch.egg-info}/PKG-INFO +1 -1
  10. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pyproject.toml +1 -1
  11. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_flat_earth_phase.py +84 -14
  12. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/LICENSE +0 -0
  13. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/MANIFEST.in +0 -0
  14. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/README.md +0 -0
  15. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/__init__.py +0 -0
  16. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/auth_commands.py +0 -0
  17. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/config_commands.py +0 -0
  18. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/download_commands.py +0 -0
  19. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/index_commands.py +0 -0
  20. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/pipeline_process_commands.py +0 -0
  21. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/postprocess_commands.py +0 -0
  22. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/preprocess_commands.py +0 -0
  23. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/sar_commands.py +0 -0
  24. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/cli/search_commands.py +0 -0
  25. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/config/__init__.py +0 -0
  26. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/config/defaults.yaml +0 -0
  27. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/config/settings.py +0 -0
  28. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/core/__init__.py +0 -0
  29. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/core/authenticator.py +0 -0
  30. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/core/cache_manager.py +0 -0
  31. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/core/downloader.py +0 -0
  32. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/core/engine.py +0 -0
  33. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/core/logging.py +0 -0
  34. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/core/orbits.py +0 -0
  35. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/core/scheduler.py +0 -0
  36. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/core/searcher.py +0 -0
  37. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/__init__.py +0 -0
  38. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/annotation.py +0 -0
  39. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/atmosphere.py +0 -0
  40. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/coregister.py +0 -0
  41. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/deburst.py +0 -0
  42. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/extraction.py +0 -0
  43. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/geolocation.py +0 -0
  44. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/gpu.py +0 -0
  45. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/ionosphere.py +0 -0
  46. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/splitspectrum_diagnostic.py +0 -0
  47. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/synthetic.py +0 -0
  48. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/timeseries.py +0 -0
  49. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/unwrap.py +0 -0
  50. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/validate.py +0 -0
  51. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/visualize.py +0 -0
  52. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/insar/workflow.py +0 -0
  53. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/models/__init__.py +0 -0
  54. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/models/download_task.py +0 -0
  55. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/models/satellite_data.py +0 -0
  56. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/models/search_query.py +0 -0
  57. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/models/user_auth.py +0 -0
  58. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processing/__init__.py +0 -0
  59. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processing/base.py +0 -0
  60. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processing/batch.py +0 -0
  61. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processing/indices.py +0 -0
  62. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processing/pipeline.py +0 -0
  63. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processing/postprocessor.py +0 -0
  64. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processing/preprocessor.py +0 -0
  65. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processing/sar.py +0 -0
  66. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processor/__init__.py +0 -0
  67. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processor/indices.py +0 -0
  68. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processor/landsat.py +0 -0
  69. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processor/loader.py +0 -0
  70. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processor/stacker.py +0 -0
  71. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/processor/timeseries.py +0 -0
  72. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/__init__.py +0 -0
  73. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/airbus_oneatlas.py +0 -0
  74. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
  75. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/aws_earth.py +0 -0
  76. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/base.py +0 -0
  77. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/copernicus.py +0 -0
  78. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/digitalglobe.py +0 -0
  79. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/earth_explorer_additional.py +0 -0
  80. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/element84.py +0 -0
  81. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/eodag_provider.py +0 -0
  82. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/esa_scihub.py +0 -0
  83. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/geoserver_generic.py +0 -0
  84. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/google_earth_engine.py +0 -0
  85. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/inpe_cbers.py +0 -0
  86. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/isro_bhuvan.py +0 -0
  87. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/jaxa_earth.py +0 -0
  88. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/maxar_gbdx.py +0 -0
  89. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/nasa_earthdata.py +0 -0
  90. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
  91. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/noaa_big_data.py +0 -0
  92. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/opentopography.py +0 -0
  93. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/planet.py +0 -0
  94. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/planetary_computer.py +0 -0
  95. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/sentinel_hub.py +0 -0
  96. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/terrabotics.py +0 -0
  97. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/providers/usgs.py +0 -0
  98. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/sar/__init__.py +0 -0
  99. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/sar/_native.py +0 -0
  100. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/sar/_ost.py +0 -0
  101. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/sar/_sarxarray.py +0 -0
  102. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/sar/extraction.py +0 -0
  103. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/sar/processor.py +0 -0
  104. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/utils/__init__.py +0 -0
  105. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/utils/file_utils.py +0 -0
  106. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/utils/gcp_recovery.py +0 -0
  107. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/utils/geo_utils.py +0 -0
  108. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/utils/logging_setup.py +0 -0
  109. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/utils/retry_handler.py +0 -0
  110. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/utils/validators.py +0 -0
  111. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/viz/__init__.py +0 -0
  112. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/viz/map.py +0 -0
  113. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch/viz/plot.py +0 -0
  114. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch.egg-info/SOURCES.txt +0 -0
  115. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch.egg-info/dependency_links.txt +0 -0
  116. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch.egg-info/entry_points.txt +0 -0
  117. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch.egg-info/not-zip-safe +0 -0
  118. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch.egg-info/requires.txt +0 -0
  119. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/pygeofetch.egg-info/top_level.txt +0 -0
  120. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/setup.cfg +0 -0
  121. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_atmospheric_circular_regression.py +0 -0
  122. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_burst_metadata.py +0 -0
  123. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_burst_processing_integration.py +0 -0
  124. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_era5_atmospheric_correction.py +0 -0
  125. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_extraction.py +0 -0
  126. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_gcp_reprojection.py +0 -0
  127. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_insar.py +0 -0
  128. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_ionosphere.py +0 -0
  129. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_models.py +0 -0
  130. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_pgv_integration_contract.py +0 -0
  131. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_provider_geometry_audit (1).py +0 -0
  132. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_provider_geometry_audit.py +0 -0
  133. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_provider_scene_disambiguation.py +0 -0
  134. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_providers.py +0 -0
  135. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_search_download.py +0 -0
  136. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_splitspectrum_diagnostic.py +0 -0
  137. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/tests/test_utils.py +0 -0
  138. {pygeofetch-1.9.6 → pygeofetch-1.9.7}/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.6/).
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.9.7/).
5
5
 
6
- ## [1.9.6] — 2026-07-29
6
+ ## [1.9.7] — 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.6/).
51
51
 
52
52
  ### Migration Guide
53
53
  ```python
54
- # Before (v1.9.6)
54
+ # Before (v1.9.7)
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.6] — 2026-07-12
75
+ ## [1.9.7] — 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.6** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
192
- | **1.9.6** | 2026-07-12 | 22 providers, 17 indices, Sentinel-1C/D |
191
+ | **1.9.7** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
192
+ | **1.9.7** | 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: 1.9.6
3
+ Version: 1.9.7
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 v1.9.6 — Universal Satellite Data Pipeline.
2
+ PyGeoFetch v1.9.7 — 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.6"
33
+ __version__ = "1.9.7"
34
34
  __author__ = "PyGeoFetch Contributors"
35
35
  __license__ = "MIT"
36
36
 
@@ -1,5 +1,5 @@
1
1
  """
2
- PyGeoFetch CLI v1.9.6 main entry point.
2
+ PyGeoFetch CLI v1.9.7 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.6 — Universal Satellite Data Pipeline
74
+ PyGeoFetch v1.9.7 — Universal Satellite Data Pipeline
75
75
  ===========================================================
76
76
  Unified access to 22+ satellite data providers.
77
77
 
@@ -172,6 +172,20 @@ def estimate_esd_shift_per_burst_overlap(
172
172
  overlaps = compute_overlap_row_ranges(swath_timing, azimuth_time_interval_s)
173
173
  lines_per_burst = swath_timing.lines_per_burst
174
174
  per_overlap_shifts: List[Optional[float]] = []
175
+ # Real, confirmed diagnostic gap fixed here: the two real, genuinely
176
+ # different reasons an overlap gets skipped (it fell entirely outside
177
+ # the given, possibly-cropped arrays -- an AOI/cropping issue -- vs.
178
+ # it was inside the arrays but below coherence_threshold -- a real
179
+ # decorrelation issue, e.g. vegetated/mountainous terrain) were
180
+ # previously indistinguishable from the outside: individual reasons
181
+ # were only logged at DEBUG level, and the final summary warning
182
+ # collapsed both into one "outside the given arrays, or were below
183
+ # coherence_threshold" message. That ambiguity is exactly what made
184
+ # it impossible to tell, from a normal INFO-level log, whether a
185
+ # given real run's ESD failure was fixable by widening the AOI or
186
+ # was a genuine reflection of the terrain -- tracked explicitly here
187
+ # instead of guessed at after the fact.
188
+ skip_reasons: List[str] = []
175
189
 
176
190
  for i, ((bw_start, bw_end), (fw_start, fw_end)) in enumerate(overlaps):
177
191
  bw_full_scene_start = i * lines_per_burst - row_offset + bw_start
@@ -184,11 +198,13 @@ def estimate_esd_shift_per_burst_overlap(
184
198
 
185
199
  if bw_clip_start >= bw_clip_end or fw_clip_start >= fw_clip_end:
186
200
  per_overlap_shifts.append(None)
201
+ skip_reasons.append("outside_array_bounds")
187
202
  continue # this overlap falls entirely outside the real, given (possibly cropped) arrays
188
203
 
189
204
  n_common = min(bw_clip_end - bw_clip_start, fw_clip_end - fw_clip_start)
190
205
  if n_common < 1:
191
206
  per_overlap_shifts.append(None)
207
+ skip_reasons.append("outside_array_bounds")
192
208
  continue
193
209
 
194
210
  ref_bw = ref_complex[bw_clip_start:bw_clip_start + n_common]
@@ -256,19 +272,45 @@ def estimate_esd_shift_per_burst_overlap(
256
272
  i, i + 1, 100 * valid.mean(), coherence_threshold,
257
273
  )
258
274
  per_overlap_shifts.append(None)
275
+ skip_reasons.append(f"low_coherence({100 * valid.mean():.0f}%)")
259
276
  continue
260
277
 
261
278
  double_diff = igram_fw * np.conj(igram_bw)
262
279
  mean_phase = np.angle(np.mean(double_diff[valid]))
263
280
  shift_s = mean_phase / (2 * np.pi * delta_f_ovl_hz)
264
281
  per_overlap_shifts.append(float(shift_s))
282
+ skip_reasons.append("used")
265
283
 
266
284
  valid_shifts = [s for s in per_overlap_shifts if s is not None]
267
285
  if not valid_shifts:
286
+ # Real, surfaced breakdown, not the previous ambiguous message --
287
+ # tells the caller directly whether every real overlap failed
288
+ # because none existed within the (possibly cropped) arrays at
289
+ # all (an AOI/cropping problem, fixable by widening the crop) or
290
+ # because they existed but were genuinely below the coherence
291
+ # threshold (a real decorrelation problem in the data itself,
292
+ # not fixable by widening anything).
293
+ n_outside = skip_reasons.count("outside_array_bounds")
294
+ n_low_coh = sum(1 for r in skip_reasons if r.startswith("low_coherence"))
268
295
  logger.warning(
269
296
  "Real per-burst-overlap ESD found no usable burst overlaps "
270
- "(all fell outside the given arrays, or were below "
271
- "coherence_threshold) — no shift estimate available."
297
+ "out of %d total: %d fell outside the given (possibly "
298
+ "cropped) arrays, %d were below coherence_threshold=%.2f. "
299
+ "%s",
300
+ len(overlaps), n_outside, n_low_coh, coherence_threshold,
301
+ (
302
+ "Every overlap fell outside the array -- try widening the "
303
+ "AOI/crop, since none were even tested for coherence."
304
+ if n_outside == len(overlaps) and len(overlaps) > 0
305
+ else (
306
+ "Every real, in-bounds overlap was tested and found "
307
+ "genuinely below the coherence threshold -- this "
308
+ "reflects real decorrelation in the data, not "
309
+ "something a larger AOI would fix."
310
+ if n_low_coh == len(overlaps) and len(overlaps) > 0
311
+ else "A mix of both -- see the per-overlap detail at DEBUG level."
312
+ )
313
+ ),
272
314
  )
273
315
  return None, per_overlap_shifts
274
316
 
@@ -279,4 +321,4 @@ def estimate_esd_shift_per_burst_overlap(
279
321
  len(valid_shifts), len(overlaps), combined_shift_s * 1000,
280
322
  combined_shift_s / azimuth_time_interval_s,
281
323
  )
282
- return combined_shift_s, per_overlap_shifts
324
+ return combined_shift_s, per_overlap_shifts
@@ -178,6 +178,39 @@ def compute_flat_earth_phase(
178
178
  n_out_of_bounds += 1
179
179
  continue
180
180
 
181
+ # Real, confirmed gap fixed here: t_ref/ref_row/ref_col were
182
+ # already validated against the reference scene's real
183
+ # bounds above, but t_sec was never checked against the
184
+ # SECONDARY scene's real bounds at all -- find_zero_doppler_time
185
+ # has no built-in guarantee of converging to the zero-Doppler
186
+ # crossing nearest the initial guess specifically (only that
187
+ # it converges to *some* zero crossing), so nothing here was
188
+ # catching a secondary-orbit convergence result that fell
189
+ # outside this ground point's real, valid azimuth/range window
190
+ # for the secondary acquisition. An undetected error here
191
+ # would silently corrupt R_sec and, through it, every sample
192
+ # point's flat_phase and the polynomial fit fit across all of
193
+ # them -- a real, plausible explanation for this project's
194
+ # own observed flat-earth phase anomaly (values in the tens
195
+ # of millions of radians, physically equivalent to tens of
196
+ # km of spurious range error, for specific real pairs).
197
+ sec_row = sec_geometry.row_for_azimuth_time(t_sec)
198
+ range_sec_time = 2 * R_sec / 299792458.0
199
+ sec_col = sec_geometry.col_for_range_time(range_sec_time)
200
+
201
+ if not (0 <= sec_row < sec_geometry.n_lines and 0 <= sec_col < sec_geometry.n_columns):
202
+ n_out_of_bounds += 1
203
+ logger.debug(
204
+ "Flat-earth phase: sample (%.4f, %.4f) rejected -- "
205
+ "t_sec converged to a secondary-scene row/col "
206
+ "(%.1f, %.1f) outside the real secondary SLC extent "
207
+ "(%d lines x %d columns). This is the real, specific "
208
+ "case the reference-only bounds check upstream could "
209
+ "not catch.",
210
+ lon, lat, sec_row, sec_col, sec_geometry.n_lines, sec_geometry.n_columns,
211
+ )
212
+ continue
213
+
181
214
  # Real, verified sign convention: interferogram phase =
182
215
  # phase(ref) - phase(sec), and SAR phase for a point at
183
216
  # range R is -(4*pi/lambda)*R, so
@@ -240,4 +273,4 @@ def compute_flat_earth_phase(
240
273
  len(grid_rows), polynomial_degree, dense_phase.min(), dense_phase.max(),
241
274
  )
242
275
 
243
- return dense_phase.astype(np.float32)
276
+ return dense_phase.astype(np.float32)
@@ -391,11 +391,32 @@ class InterferogramGenerator:
391
391
 
392
392
  # Step 2: real deburst, applied to both images using each
393
393
  # date's own real burst metadata.
394
+ #
395
+ # Real, confirmed bug fixed here: by this point sec_complex has
396
+ # already been resampled onto the REFERENCE's real grid, either
397
+ # by _orbit_based_coregister (which explicitly fits and applies
398
+ # an offset field using both ref_row_off AND sec_row_off
399
+ # together, see resample_with_offset_field above) or by the
400
+ # shape-based _resample_to_reference fallback -- either way,
401
+ # sec_complex's row 0 now corresponds to the same real
402
+ # full-scene row as ref_complex's row 0, i.e. ref_row_off, not
403
+ # its own original, pre-coregistration sec_row_off. Passing
404
+ # the stale sec_row_off here fed deburst_array a coordinate
405
+ # system that no longer matched the array it was actually
406
+ # operating on, silently shifting which rows got cropped as
407
+ # "burst edge/overlap" for the secondary relative to where the
408
+ # real burst boundaries actually fell in the now-coregistered
409
+ # array -- corrupting the debursted secondary image used for
410
+ # every downstream step (interferogram formation, coherence,
411
+ # unwrapping), independent of and in addition to the separate
412
+ # per-burst-overlap ESD issue investigated above, which runs
413
+ # earlier, on the pre-deburst array, and was already using
414
+ # ref_row_off correctly for both images at that stage.
394
415
  ref_debursted, ref_first_kept_row = deburst_array(
395
416
  ref_complex, ref_burst_info, azimuth_time_interval_s, row_offset=int(ref_row_off)
396
417
  )
397
418
  sec_debursted, _ = deburst_array(
398
- sec_complex, sec_burst_info, azimuth_time_interval_s, row_offset=int(sec_row_off)
419
+ sec_complex, sec_burst_info, azimuth_time_interval_s, row_offset=int(ref_row_off)
399
420
  )
400
421
 
401
422
  # Real correctness fix, not optional: deburst can remove
@@ -549,6 +570,63 @@ class InterferogramGenerator:
549
570
  DataValidator.validate_slc(ref_complex, name="reference SLC").raise_if_invalid()
550
571
  DataValidator.validate_slc(sec_complex, name="secondary SLC").raise_if_invalid()
551
572
 
573
+ # Real, confirmed-relevant check added here: mismatched satellite
574
+ # platform (S1A vs S1B) or sub-swath (IW1/IW2/IW3) between the two
575
+ # real dates is a genuine, documented risk factor for this exact
576
+ # kind of pair -- confirmed directly against ESA's own S1TBX TOPS
577
+ # interferometry tutorial: sub-swaths are processed independently
578
+ # and must be explicitly merged (S-1 TOPS Merge) before combining
579
+ # data from different ones, and combining S1A/S1B "be aware the
580
+ # extents... can be shifted along-track and different bursts must
581
+ # be selected for the same area." Real, satellite platform is read
582
+ # directly from the .SAFE product's own real filename (its first
583
+ # three characters, "S1A" or "S1B", by Sentinel-1's own naming
584
+ # convention), and real sub-swath from the tag SLCExtractor itself
585
+ # already records at extraction time -- not guessed. This does not
586
+ # raise: pygeofetch's own coregistration and burst-aware processing
587
+ # can still run and may still produce a usable result, but a
588
+ # mismatch here is a real, direct, and previously silent
589
+ # contributor to degraded coregistration and failed per-burst-
590
+ # overlap ESD, worth surfacing loudly rather than only showing up
591
+ # later as an unexplained low coherence value.
592
+ if reference_safe_zip is not None and secondary_safe_zip is not None:
593
+ ref_platform = Path(reference_safe_zip).name[:3]
594
+ sec_platform = Path(secondary_safe_zip).name[:3]
595
+ if ref_platform != sec_platform:
596
+ logger.warning(
597
+ "Reference (%s) and secondary (%s) scenes are from "
598
+ "different Sentinel-1 satellites -- burst boundaries "
599
+ "for the same real ground area are not guaranteed to "
600
+ "align between S1A and S1B. This pair can still be "
601
+ "processed, but expect degraded coregistration and "
602
+ "possible per-burst-overlap ESD failure as a direct "
603
+ "consequence, not a separate, unrelated issue.",
604
+ ref_platform, sec_platform,
605
+ )
606
+
607
+ try:
608
+ from pygeofetch.insar.coregister import read_matched_swath
609
+
610
+ ref_swath = read_matched_swath(reference) if reference is not None else None
611
+ sec_swath = read_matched_swath(secondary) if secondary is not None else None
612
+ if ref_swath and sec_swath and ref_swath != sec_swath:
613
+ logger.warning(
614
+ "Reference (%s) and secondary (%s) scenes were "
615
+ "extracted from different real sub-swaths -- per "
616
+ "ESA's own TOPS documentation, sub-swaths are "
617
+ "processed independently and require an explicit "
618
+ "merge step before combining; without it, ESD "
619
+ "and coregistration between these two dates are "
620
+ "not expected to find a usable, shared burst "
621
+ "structure. This pair can still be processed, "
622
+ "but this mismatch is a real, direct, and "
623
+ "previously silent explanation if ESD reports no "
624
+ "usable overlaps for it.",
625
+ ref_swath, sec_swath,
626
+ )
627
+ except Exception:
628
+ pass # real, best-effort check -- never block processing over it
629
+
552
630
  # Step 1: coregistration.
553
631
  real_coreg_inputs = (
554
632
  dem, reference_safe_zip, secondary_safe_zip,
@@ -1079,16 +1157,72 @@ class InterferogramGenerator:
1079
1157
  return interferogram, {"correction_applied": False, "reason": "rasterio_missing"}
1080
1158
 
1081
1159
  try:
1160
+ interferogram_crs = profile.get("crs")
1161
+ interferogram_transform = profile.get("transform")
1162
+
1082
1163
  with rasterio.open(dem_path) as dem_src:
1083
- dem = dem_src.read(1).astype(np.float32)
1084
- if dem.shape != interferogram.shape:
1085
- from scipy.ndimage import zoom
1164
+ if interferogram_crs is not None and interferogram_transform is not None:
1165
+ # Real, confirmed bug fixed here: the previous fallback
1166
+ # reprojected the DEM onto the interferogram's grid
1167
+ # using ONLY a shape ratio (scipy.ndimage.zoom), never
1168
+ # referencing the real, georeferenced profile/transform
1169
+ # this method receives as its own `profile` argument --
1170
+ # that only produces a correctly-aligned DEM if the DEM
1171
+ # and interferogram happen to cover exactly the same
1172
+ # real geographic extent, which this project's own AOI-
1173
+ # cropped, orbit-coregistered pipeline does not
1174
+ # guarantee (confirmed directly against this project's
1175
+ # own logs: reference/secondary crop offsets of several
1176
+ # thousand rows relative to each other). A shape-only
1177
+ # resample under that mismatch compares elevation at
1178
+ # the wrong real pixels to phase at the wrong real
1179
+ # pixels, which would show up as exactly the near-zero
1180
+ # or negative R^2 this project's own real runs
1181
+ # reported for every pair tested -- not necessarily
1182
+ # because there was little true topographic phase to
1183
+ # find, but because the regression was never actually
1184
+ # comparing elevation and phase at the same ground
1185
+ # location to begin with. Reproject properly using the
1186
+ # real CRS/transform on both sides instead.
1187
+ from rasterio.warp import reproject, Resampling
1188
+
1189
+ dem = np.empty(interferogram.shape, dtype=np.float32)
1190
+ reproject(
1191
+ source=rasterio.band(dem_src, 1),
1192
+ destination=dem,
1193
+ src_transform=dem_src.transform,
1194
+ src_crs=dem_src.crs,
1195
+ dst_transform=interferogram_transform,
1196
+ dst_crs=interferogram_crs,
1197
+ resampling=Resampling.bilinear,
1198
+ src_nodata=dem_src.nodata,
1199
+ dst_nodata=np.nan,
1200
+ )
1201
+ else:
1202
+ # Honest fallback: no real georeferencing available on
1203
+ # either side to reproject against, so approximate
1204
+ # alignment via shape ratio is the best available --
1205
+ # but this is a real, meaningfully weaker guarantee
1206
+ # than the georeferenced path above, and worth logging
1207
+ # as such rather than silently treating it the same.
1208
+ logger.warning(
1209
+ "No real CRS/transform available on the "
1210
+ "interferogram profile -- falling back to a "
1211
+ "shape-ratio DEM resample, which only aligns "
1212
+ "correctly if the DEM and interferogram already "
1213
+ "cover the exact same real geographic extent. "
1214
+ "Topographic phase removal R^2 may be unreliable "
1215
+ "as a result."
1216
+ )
1217
+ dem = dem_src.read(1).astype(np.float32)
1218
+ if dem.shape != interferogram.shape:
1219
+ from scipy.ndimage import zoom
1086
1220
 
1087
- zf = (
1088
- interferogram.shape[0] / dem.shape[0],
1089
- interferogram.shape[1] / dem.shape[1],
1090
- )
1091
- dem = zoom(dem, zf, order=1)
1221
+ zf = (
1222
+ interferogram.shape[0] / dem.shape[0],
1223
+ interferogram.shape[1] / dem.shape[1],
1224
+ )
1225
+ dem = zoom(dem, zf, order=1)
1092
1226
 
1093
1227
  valid = np.isfinite(dem) & (dem > -500)
1094
1228
  phase = np.angle(interferogram)
@@ -1,4 +1,4 @@
1
- {% set version = "1.9.6" %}
1
+ {% set version = "1.9.7" %}
2
2
 
3
3
  package:
4
4
  name: pygeofetch
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygeofetch
3
- Version: 1.9.6
3
+ Version: 1.9.7
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygeofetch"
7
- version = "1.9.6"
7
+ version = "1.9.7"
8
8
  description = "Universal satellite data pipeline + geospatial processing platform"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -19,13 +19,12 @@ from pathlib import Path
19
19
  import numpy as np
20
20
  import pytest
21
21
 
22
- sys.path.insert(0, str(Path(__file__).parent))
23
- from test_insar_verified_components import _build_geometry # noqa: E402
24
-
25
- from pygeofetch.insar.annotation import SLCGeometry # noqa: E402
26
- from pygeofetch.insar.flatearth import compute_flat_earth_phase # noqa: E402
27
- from pygeofetch.insar.geolocation import ( # noqa: E402
22
+ from pygeofetch.insar.annotation import SLCGeometry
23
+ from pygeofetch.insar.flatearth import compute_flat_earth_phase
24
+ from pygeofetch.insar.geolocation import (
28
25
  SPEED_OF_LIGHT,
26
+ WGS84_A,
27
+ WGS84_B,
29
28
  geodetic_to_ecef,
30
29
  find_zero_doppler_time,
31
30
  interpolate_orbit_state,
@@ -34,6 +33,62 @@ from pygeofetch.insar.geolocation import ( # noqa: E402
34
33
  WAVELENGTH_M = 0.05546576
35
34
 
36
35
 
36
+ # Real, confirmed fix here: this file previously imported _build_geometry
37
+ # from a sibling test module (test_insar_verified_components) via a
38
+ # runtime sys.path.insert() hack. That only worked when both files sat
39
+ # together in the same directory -- a real, brittle assumption that broke
40
+ # in any environment where this file is used standalone (as reported: the
41
+ # sibling file genuinely does not exist there). Inlined below, verbatim,
42
+ # so this file is fully self-contained and has no dependency on any
43
+ # other test file existing.
44
+ def _build_geometry(lat_deg, lon_deg, dem_h=0.0, incl_deg=98.18, ascending=True):
45
+ """Build a fully self-consistent, realistic satellite+ground-point test
46
+ geometry — real orbital velocity direction, exact zero-Doppler
47
+ projection."""
48
+ lat, lon = math.radians(lat_deg), math.radians(lon_deg)
49
+ e2 = 1 - ((WGS84_B + dem_h) ** 2 / (WGS84_A + dem_h) ** 2)
50
+ N = (WGS84_A + dem_h) / math.sqrt(1 - e2 * math.sin(lat) ** 2)
51
+ P_true = (
52
+ N * math.cos(lat) * math.cos(lon),
53
+ N * math.cos(lat) * math.sin(lon),
54
+ N * (1 - e2) * math.sin(lat),
55
+ )
56
+ altitude = 693000.0
57
+ normal = (
58
+ P_true[0] / (WGS84_A + dem_h) ** 2,
59
+ P_true[1] / (WGS84_A + dem_h) ** 2,
60
+ P_true[2] / (WGS84_B + dem_h) ** 2,
61
+ )
62
+ nmag = math.sqrt(sum(c**2 for c in normal))
63
+ normal = tuple(c / nmag for c in normal)
64
+ sat_pos = tuple(P_true[i] + normal[i] * altitude for i in range(3))
65
+
66
+ incl = math.radians(incl_deg)
67
+ sign = 1 if ascending else -1
68
+ plane_normal = (
69
+ sign * math.sin(incl) * math.sin(lon),
70
+ -sign * math.sin(incl) * math.cos(lon),
71
+ math.cos(incl),
72
+ )
73
+ pn_mag = math.sqrt(sum(c**2 for c in plane_normal))
74
+ plane_normal = tuple(c / pn_mag for c in plane_normal)
75
+ tangent = (
76
+ plane_normal[1] * normal[2] - plane_normal[2] * normal[1],
77
+ plane_normal[2] * normal[0] - plane_normal[0] * normal[2],
78
+ plane_normal[0] * normal[1] - plane_normal[1] * normal[0],
79
+ )
80
+ tmag = math.sqrt(sum(c**2 for c in tangent))
81
+ tangent = tuple(c / tmag for c in tangent)
82
+ sat_vel = tuple(c * 7500.0 for c in tangent)
83
+
84
+ d = sum(sat_vel[i] * (P_true[i] - sat_pos[i]) for i in range(3)) / sum(v**2 for v in sat_vel)
85
+ P_exact = tuple(P_true[i] - d * sat_vel[i] for i in range(3))
86
+ los = tuple(sat_pos[i] - P_exact[i] for i in range(3))
87
+ range_m = math.sqrt(sum(c**2 for c in los))
88
+ range_time_s = 2 * range_m / SPEED_OF_LIGHT
89
+ return sat_pos, sat_vel, range_time_s, P_exact
90
+
91
+
37
92
  def _orbit_series(center_pos, center_vel, center_time):
38
93
  times, positions, velocities = [], [], []
39
94
  for i in range(-60, 61):
@@ -59,7 +114,22 @@ def _build_fixture(n=200, b_perp=80.0):
59
114
  near_range_time_s=range_time_center - (n / 2) * (1 / 6.4e7), range_sampling_rate_hz=6.4e7,
60
115
  n_lines=n, n_columns=n,
61
116
  )
62
- return ref_geom, ref_orbit, sec_orbit, t0_ref, t0_sec, n
117
+ # Real, confirmed test-fixture gap fixed here: the fixture previously
118
+ # only built ONE SLCGeometry and every caller passed it as BOTH
119
+ # ref_geometry and sec_geometry -- which never exercised the real,
120
+ # production scenario (interferogram.py always parses a genuinely
121
+ # separate sec_geom_fe from the secondary's own SAFE zip, anchored to
122
+ # the secondary's own real first_line_time, not the reference's).
123
+ # Reusing ref_geom for both meant t_sec (anchored ~12 real days after
124
+ # ref_geom's own first_line_time) was silently being evaluated
125
+ # against the WRONG scene's timing reference whenever a real
126
+ # secondary-bounds check was added -- exactly what surfaced this gap.
127
+ sec_geom = SLCGeometry(
128
+ first_line_time=t0_sec - timedelta(seconds=n / 2 * 0.002), azimuth_time_interval_s=0.002,
129
+ near_range_time_s=range_time_center - (n / 2) * (1 / 6.4e7), range_sampling_rate_hz=6.4e7,
130
+ n_lines=n, n_columns=n,
131
+ )
132
+ return ref_geom, sec_geom, ref_orbit, sec_orbit, t0_ref, t0_sec, n
63
133
 
64
134
 
65
135
  def test_range_difference_matches_textbook_approximation():
@@ -98,11 +168,11 @@ def test_matches_independent_direct_computation_at_a_real_point():
98
168
  """The module's polynomial-fit output at a specific pixel must
99
169
  match a completely independent, direct geometric computation at
100
170
  that same real point."""
101
- ref_geom, ref_orbit, sec_orbit, t0_ref, t0_sec, n = _build_fixture()
171
+ ref_geom, sec_geom, ref_orbit, sec_orbit, t0_ref, t0_sec, n = _build_fixture()
102
172
  lon0, lat0 = -99.11, 19.34
103
173
 
104
174
  flat_phase = compute_flat_earth_phase(
105
- ref_geom, ref_orbit, ref_geom, sec_orbit, t0_ref, t0_sec, (n, n), WAVELENGTH_M,
175
+ ref_geom, ref_orbit, sec_geom, sec_orbit, t0_ref, t0_sec, (n, n), WAVELENGTH_M,
106
176
  sample_bounds=(lon0, lat0, lon0 + 0.04, lat0 + 0.04), grid_points=7, polynomial_degree=2,
107
177
  )
108
178
 
@@ -129,11 +199,11 @@ def test_closed_loop_removal_through_complex_wrapped_pathway():
129
199
  a real, complex, wrapped interferogram alongside a known
130
200
  deformation signal, remove it, and confirm exact recovery of the
131
201
  true signal -- the same pathway process_pair() actually uses."""
132
- ref_geom, ref_orbit, sec_orbit, t0_ref, t0_sec, n = _build_fixture()
202
+ ref_geom, sec_geom, ref_orbit, sec_orbit, t0_ref, t0_sec, n = _build_fixture()
133
203
  lon0, lat0 = -99.11, 19.34
134
204
 
135
205
  flat_phase = compute_flat_earth_phase(
136
- ref_geom, ref_orbit, ref_geom, sec_orbit, t0_ref, t0_sec, (n, n), WAVELENGTH_M,
206
+ ref_geom, ref_orbit, sec_geom, sec_orbit, t0_ref, t0_sec, (n, n), WAVELENGTH_M,
137
207
  sample_bounds=(lon0, lat0, lon0 + 0.04, lat0 + 0.04), grid_points=7, polynomial_degree=2,
138
208
  )
139
209
 
@@ -155,8 +225,8 @@ def test_closed_loop_removal_through_complex_wrapped_pathway():
155
225
 
156
226
 
157
227
  def test_raises_with_neither_dem_nor_sample_bounds():
158
- ref_geom, ref_orbit, sec_orbit, t0_ref, t0_sec, n = _build_fixture()
228
+ ref_geom, sec_geom, ref_orbit, sec_orbit, t0_ref, t0_sec, n = _build_fixture()
159
229
  with pytest.raises(ValueError):
160
230
  compute_flat_earth_phase(
161
- ref_geom, ref_orbit, ref_geom, sec_orbit, t0_ref, t0_sec, (n, n), WAVELENGTH_M,
162
- )
231
+ ref_geom, ref_orbit, sec_geom, sec_orbit, t0_ref, t0_sec, (n, n), WAVELENGTH_M,
232
+ )
File without changes
File without changes
File without changes
File without changes