PyGeoFetch 2.6.2.3__tar.gz → 2.6.2.5__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 (235) hide show
  1. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/CHANGELOG.md +7 -7
  2. {pygeofetch-2.6.2.3/pygeofetch.egg-info → pygeofetch-2.6.2.5}/PKG-INFO +5 -10
  3. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/README.md +1 -4
  4. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/__init__.py +1 -1
  5. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/main.py +4 -0
  6. pygeofetch-2.6.2.5/pygeofetch/cli/multisensor_commands.py +243 -0
  7. pygeofetch-2.6.2.5/pygeofetch/cli/optical_commands.py +105 -0
  8. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/search_commands.py +121 -8
  9. pygeofetch-2.6.2.5/pygeofetch/core/data_organizer.py +364 -0
  10. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/engine.py +8 -4
  11. pygeofetch-2.6.2.5/pygeofetch/insar/advanced_safeguards.py +370 -0
  12. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/preflight.py +136 -1
  13. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/meta.yaml +1 -1
  14. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/models/satellite_data.py +49 -0
  15. pygeofetch-2.6.2.5/pygeofetch/multisensor/dem_differencing.py +201 -0
  16. pygeofetch-2.6.2.5/pygeofetch/multisensor/flood_mapping.py +215 -0
  17. pygeofetch-2.6.2.5/pygeofetch/multisensor/insar_optical_fusion.py +269 -0
  18. pygeofetch-2.6.2.5/pygeofetch/multisensor/multisensor_init.py +55 -0
  19. pygeofetch-2.6.2.5/pygeofetch/multisensor/terrain_corrected_change.py +285 -0
  20. pygeofetch-2.6.2.5/pygeofetch/multisensor/vegetation_disturbance.py +220 -0
  21. pygeofetch-2.6.2.5/pygeofetch/optical/__init__.py +35 -0
  22. pygeofetch-2.6.2.5/pygeofetch/optical/offset_tracking.py +789 -0
  23. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processor/indices.py +143 -11
  24. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/__init__.py +2 -4
  25. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/airbus_oneatlas.py +1 -1
  26. pygeofetch-2.6.2.5/pygeofetch/providers/alaska_satellite_facility.py +361 -0
  27. pygeofetch-2.6.2.5/pygeofetch/providers/digitalglobe.py +423 -0
  28. pygeofetch-2.6.2.5/pygeofetch/providers/earth_explorer_additional.py +104 -0
  29. pygeofetch-2.6.2.5/pygeofetch/providers/geoserver_generic.py +377 -0
  30. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/inpe_cbers.py +114 -96
  31. pygeofetch-2.6.2.5/pygeofetch/providers/isro_bhuvan.py +357 -0
  32. pygeofetch-2.6.2.5/pygeofetch/providers/jaxa_earth.py +276 -0
  33. pygeofetch-2.6.2.5/pygeofetch/providers/maxar_gbdx.py +298 -0
  34. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/nasa_earthdata.py +26 -6
  35. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/nasa_earthdata_cloud.py +114 -38
  36. pygeofetch-2.6.2.5/pygeofetch/providers/opentopography.py +435 -0
  37. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/planet.py +23 -2
  38. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/sentinel_hub.py +50 -25
  39. pygeofetch-2.6.2.5/pygeofetch/sar/__init__.py +49 -0
  40. pygeofetch-2.6.2.5/pygeofetch/sar/math.py +100 -0
  41. pygeofetch-2.6.2.5/pygeofetch/sar/pipelines.py +573 -0
  42. pygeofetch-2.6.2.5/pygeofetch/sar/processor.py +419 -0
  43. pygeofetch-2.6.2.5/pygeofetch/utils/sar_math.py +100 -0
  44. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/validation/optical_validator.py +69 -33
  45. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/viz/plot.py +24 -5
  46. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5/pygeofetch.egg-info}/PKG-INFO +5 -10
  47. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch.egg-info/SOURCES.txt +43 -0
  48. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch.egg-info/requires.txt +2 -5
  49. pygeofetch-2.6.2.5/pygeofetch.egg-info/top_level.txt +2 -0
  50. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pyproject.toml +16 -16
  51. pygeofetch-2.6.2.5/tests/test_advanced_safeguards.py +167 -0
  52. pygeofetch-2.6.2.5/tests/test_alaska_satellite_facility.py +217 -0
  53. pygeofetch-2.6.2.5/tests/test_build_sbas_network.py +191 -0
  54. pygeofetch-2.6.2.5/tests/test_coregister_upgrades.py +295 -0
  55. pygeofetch-2.6.2.5/tests/test_data_organizer.py +170 -0
  56. pygeofetch-2.6.2.5/tests/test_dem_differencing_pipeline.py +168 -0
  57. pygeofetch-2.6.2.5/tests/test_digitalglobe.py +260 -0
  58. pygeofetch-2.6.2.5/tests/test_earth_explorer_additional.py +94 -0
  59. pygeofetch-2.6.2.5/tests/test_flood_mapping_pipeline.py +229 -0
  60. pygeofetch-2.6.2.5/tests/test_geology_indices_and_rvi_fix.py +159 -0
  61. pygeofetch-2.6.2.5/tests/test_geoserver_generic.py +207 -0
  62. pygeofetch-2.6.2.5/tests/test_inpe_cbers.py +170 -0
  63. pygeofetch-2.6.2.5/tests/test_insar_optical_fusion_pipeline.py +183 -0
  64. pygeofetch-2.6.2.5/tests/test_isro_bhuvan.py +227 -0
  65. pygeofetch-2.6.2.5/tests/test_jaxa_earth.py +173 -0
  66. pygeofetch-2.6.2.5/tests/test_nasa_earthdata_cloud_s3_fix.py +173 -0
  67. pygeofetch-2.6.2.5/tests/test_nasa_earthdata_public_search.py +81 -0
  68. pygeofetch-2.6.2.5/tests/test_opentopography_pointcloud_usgs.py +233 -0
  69. pygeofetch-2.6.2.5/tests/test_optical_offset_tracking.py +183 -0
  70. pygeofetch-2.6.2.5/tests/test_optical_validator_stac.py +223 -0
  71. pygeofetch-2.6.2.5/tests/test_planet_asset_type_fix.py +109 -0
  72. pygeofetch-2.6.2.5/tests/test_preflight_spatial_baseline.py +152 -0
  73. pygeofetch-2.6.2.5/tests/test_provider_geometry_audit.py +186 -0
  74. pygeofetch-2.6.2.5/tests/test_sar_pipelines.py +317 -0
  75. pygeofetch-2.6.2.5/tests/test_sentinel_hub_catalog_fix.py +124 -0
  76. pygeofetch-2.6.2.5/tests/test_strain_and_fusion.py +252 -0
  77. pygeofetch-2.6.2.5/tests/test_terrain_corrected_change_pipeline.py +210 -0
  78. pygeofetch-2.6.2.5/tests/test_vegetation_disturbance_pipeline.py +242 -0
  79. pygeofetch-2.6.2.3/pygeofetch/providers/alaska_satellite_facility.py +0 -256
  80. pygeofetch-2.6.2.3/pygeofetch/providers/digitalglobe.py +0 -254
  81. pygeofetch-2.6.2.3/pygeofetch/providers/earth_explorer_additional.py +0 -254
  82. pygeofetch-2.6.2.3/pygeofetch/providers/geoserver_generic.py +0 -256
  83. pygeofetch-2.6.2.3/pygeofetch/providers/isro_bhuvan.py +0 -254
  84. pygeofetch-2.6.2.3/pygeofetch/providers/jaxa_earth.py +0 -256
  85. pygeofetch-2.6.2.3/pygeofetch/providers/maxar_gbdx.py +0 -266
  86. pygeofetch-2.6.2.3/pygeofetch/providers/opentopography.py +0 -239
  87. pygeofetch-2.6.2.3/pygeofetch/sar/__init__.py +0 -22
  88. pygeofetch-2.6.2.3/pygeofetch/sar/processor.py +0 -145
  89. pygeofetch-2.6.2.3/pygeofetch.egg-info/top_level.txt +0 -1
  90. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/LICENSE +0 -0
  91. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/MANIFEST.in +0 -0
  92. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/__init__.py +0 -0
  93. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/auth_commands.py +0 -0
  94. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/config_commands.py +0 -0
  95. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/download_commands.py +0 -0
  96. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/index_commands.py +0 -0
  97. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/monitor_commands.py +0 -0
  98. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/pipeline_process_commands.py +0 -0
  99. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/postprocess_commands.py +0 -0
  100. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/preprocess_commands.py +0 -0
  101. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/cli/sar_commands.py +0 -0
  102. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/config/__init__.py +0 -0
  103. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/config/defaults.yaml +0 -0
  104. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/config/settings.py +0 -0
  105. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/__init__.py +0 -0
  106. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/authenticator.py +0 -0
  107. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/cache_manager.py +0 -0
  108. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/downloader.py +0 -0
  109. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/logging.py +0 -0
  110. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/monitor.py +0 -0
  111. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/orbits.py +0 -0
  112. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/scheduler.py +0 -0
  113. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/searcher.py +0 -0
  114. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/core/state.py +0 -0
  115. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/__init__.py +0 -0
  116. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/analysis.py +0 -0
  117. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/annotation.py +0 -0
  118. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/atmosphere.py +0 -0
  119. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/coregister.py +0 -0
  120. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/deburst.py +0 -0
  121. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/esd.py +0 -0
  122. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/extraction.py +0 -0
  123. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/flatearth.py +0 -0
  124. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/geolocation.py +0 -0
  125. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/gpu.py +0 -0
  126. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/interferogram.py +0 -0
  127. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/ionosphere.py +0 -0
  128. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/offset_geometry.py +0 -0
  129. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/offset_tracking.py +0 -0
  130. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/provenance.py +0 -0
  131. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/ps_selection.py +0 -0
  132. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/splitspectrum_diagnostic.py +0 -0
  133. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/stack_selection.py +0 -0
  134. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/synthetic.py +0 -0
  135. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/timeseries.py +0 -0
  136. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/unwrap.py +0 -0
  137. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/validate.py +0 -0
  138. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/visualize.py +0 -0
  139. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/insar/workflow.py +0 -0
  140. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/models/__init__.py +0 -0
  141. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/models/download_task.py +0 -0
  142. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/models/search_query.py +0 -0
  143. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/models/user_auth.py +0 -0
  144. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processing/__init__.py +0 -0
  145. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processing/base.py +0 -0
  146. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processing/batch.py +0 -0
  147. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processing/indices.py +0 -0
  148. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processing/pipeline.py +0 -0
  149. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processing/postprocessor.py +0 -0
  150. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processing/preprocessor.py +0 -0
  151. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processing/sar.py +0 -0
  152. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processor/__init__.py +0 -0
  153. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processor/landsat.py +0 -0
  154. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processor/loader.py +0 -0
  155. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processor/stacker.py +0 -0
  156. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/processor/timeseries.py +0 -0
  157. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/aws_earth.py +0 -0
  158. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/base.py +0 -0
  159. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/copernicus.py +0 -0
  160. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/copernicus_nodes.py +0 -0
  161. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/element84.py +0 -0
  162. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/eodag_provider.py +0 -0
  163. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/esa_scihub.py +0 -0
  164. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/google_earth_engine.py +0 -0
  165. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/noaa_big_data.py +0 -0
  166. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/planetary_computer.py +0 -0
  167. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/terrabotics.py +0 -0
  168. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/providers/usgs.py +0 -0
  169. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/sar/_native.py +0 -0
  170. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/sar/_ost.py +0 -0
  171. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/sar/_sarxarray.py +0 -0
  172. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/sar/extraction.py +0 -0
  173. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/utils/__init__.py +0 -0
  174. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/utils/file_utils.py +0 -0
  175. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/utils/gcp_recovery.py +0 -0
  176. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/utils/geo_utils.py +0 -0
  177. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/utils/logging_setup.py +0 -0
  178. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/utils/retry_handler.py +0 -0
  179. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/utils/validators.py +0 -0
  180. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/validation/__init__.py +0 -0
  181. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/viz/__init__.py +0 -0
  182. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch/viz/map.py +0 -0
  183. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch.egg-info/dependency_links.txt +0 -0
  184. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch.egg-info/entry_points.txt +0 -0
  185. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/pygeofetch.egg-info/not-zip-safe +0 -0
  186. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/setup.cfg +0 -0
  187. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_airbus_oneatlas.py +0 -0
  188. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_atmospheric_circular_regression.py +0 -0
  189. /pygeofetch-2.6.2.3/tests/test_build_sbas_network.py → /pygeofetch-2.6.2.5/tests/test_build_sbas_network-new.py +0 -0
  190. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_burst_metadata.py +0 -0
  191. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_burst_processing_integration.py +0 -0
  192. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_burst_sync_large_gap.py +0 -0
  193. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_burst_sync_prescreening.py +0 -0
  194. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_burst_synchronization.py +0 -0
  195. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_circuit_breaker_wiring.py +0 -0
  196. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_cli_optical_validation.py +0 -0
  197. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_copernicus_nodes.py +0 -0
  198. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_coregister_integration.py +0 -0
  199. /pygeofetch-2.6.2.3/tests/test_coregister_upgrades.py → /pygeofetch-2.6.2.5/tests/test_coregister_upgrades (1).py +0 -0
  200. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_credential_encryption.py +0 -0
  201. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_era5_atmospheric_correction.py +0 -0
  202. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_esa_scihub.py +0 -0
  203. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_extraction.py +0 -0
  204. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_flat_earth_phase.py +0 -0
  205. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_gcp_reprojection.py +0 -0
  206. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_google_earth_engine.py +0 -0
  207. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_insar.py +0 -0
  208. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_ionosphere.py +0 -0
  209. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_map_preview_consolidation.py +0 -0
  210. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_models.py +0 -0
  211. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_monitor.py +0 -0
  212. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_noaa_big_data.py +0 -0
  213. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_offset_tracking.py +0 -0
  214. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_optical_validation_wiring.py +0 -0
  215. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_optical_validator.py +0 -0
  216. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_pgv_integration_contract.py +0 -0
  217. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_pipeline_process_export.py +0 -0
  218. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_preflight_gate.py +0 -0
  219. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_provider_geometry_audit (1).py +0 -0
  220. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_provider_scene_disambiguation.py +0 -0
  221. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_providers.py +0 -0
  222. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_ps_selection.py +0 -0
  223. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_raster_collocation_integration.py +0 -0
  224. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_reference_pixel_and_despike.py +0 -0
  225. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_risk_mapping.py +0 -0
  226. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_screen_stack_orchestration.py +0 -0
  227. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_search_download.py +0 -0
  228. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_sign_convention.py +0 -0
  229. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_splitspectrum_diagnostic.py +0 -0
  230. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_stack_selection_consolidation.py +0 -0
  231. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_state.py +0 -0
  232. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_topo_r2_bug.py +0 -0
  233. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_utils.py +0 -0
  234. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_viz_plot.py +0 -0
  235. {pygeofetch-2.6.2.3 → pygeofetch-2.6.2.5}/tests/test_wls_inversion.py +0 -0
@@ -20,7 +20,7 @@ date, and marked accordingly rather than filled in with invented
20
20
  bullet points.
21
21
  ```
22
22
 
23
- ## [Unreleased] — v2.6.2.4
23
+ ## [Unreleased] — v2.6.2.5.4
24
24
 
25
25
  ```{note}
26
26
  Not yet published to PyPI. This entry documents work completed and
@@ -130,7 +130,7 @@ audit and remain open for a follow-up pass.
130
130
 
131
131
  ---
132
132
 
133
- ## [2.6.2.3] — 2026-08-25 (latest)
133
+ ## [2.6.2.5.4] — 2026-08-25 (latest)
134
134
 
135
135
  Confirmed via direct fetch of the current PyPI listing.
136
136
 
@@ -149,7 +149,7 @@ Confirmed via direct fetch of the current PyPI listing.
149
149
  - `SearchQuery.sort_ascending: bool` — earlier examples showed a `sort_order: "asc"/"desc"` string field that doesn't exist on the model (the CLI's `--sort-order` flag does convert to this internally, so CLI docs were unaffected)
150
150
  - `DownloadOptions.post_process` requires a list of `PostProcessAction` objects — passing plain strings raises a real `pydantic.ValidationError`
151
151
 
152
- ## [2.6.2] — 2026-08-24
152
+ ## [2.6.2.5] — 2026-08-24
153
153
  Real, verified release. Detailed notes not independently recovered from PyPI's release-history page (no distinct per-version notes exposed for this release specifically).
154
154
 
155
155
  ## [2.6.1] — 2026-08-22
@@ -261,9 +261,9 @@ recovered.
261
261
 
262
262
  ```{note}
263
263
  The previous version of this changelog had an entry labeled
264
- **"2.6.2.3 — 2026-07-12"** describing 22+ providers, Sentinel-1C/1D
264
+ **"2.6.2.5.4 — 2026-07-12"** describing 22+ providers, Sentinel-1C/1D
265
265
  support, SLC search with provider routing, precise orbit file
266
- management, 17 spectral indices, and various bug fixes. **2.6.2.3 was
266
+ management, 17 spectral indices, and various bug fixes. **2.6.2.5.4 was
267
267
  only ever released once, on 2026-08-25** — it was never released on
268
268
  2026-07-12. The date matches this real release, **1.0.8**, exactly.
269
269
  That content is reproduced below under the version it actually
@@ -406,8 +406,8 @@ The version of this file uploaded for this update had **5 entries
406
406
  covering 5 dates**. PyPI's real release history has **54 releases**.
407
407
  Specific issues found and fixed:
408
408
 
409
- 1. **Two different entries were both labeled `2.6.2.3`**, dated
410
- 2026-07-29 and 2026-07-12. **2.6.2.3 was only ever released once**,
409
+ 1. **Two different entries were both labeled `2.6.2.5.4`**, dated
410
+ 2026-07-29 and 2026-07-12. **2.6.2.5.4 was only ever released once**,
411
411
  on 2026-08-25 (confirmed directly from the live PyPI listing).
412
412
  2. **The 2026-07-12 entry's date exactly matches a real release**
413
413
  — version **1.0.8** — and its content (22+ providers, Sentinel-1C/D,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygeofetch
3
- Version: 2.6.2.3
3
+ Version: 2.6.2.5
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
@@ -26,7 +26,7 @@ License: MIT License
26
26
  SOFTWARE.
27
27
 
28
28
  Project-URL: Homepage, https://www.eocoreint.com/
29
- Project-URL: Documentation, https://pygeofetch.readthedocs.io/en/latest/
29
+ Project-URL: Documentation, https://appiahkubis14.github.io/pygeofetch-docs/
30
30
  Project-URL: Repository, https://github.com/EOCoreINT/pygeofetch
31
31
  Project-URL: Bug Tracker, https://github.com/EOCoreINT/pygeofetch/issues
32
32
  Keywords: satellite,remote-sensing,GIS,earth-observation,geospatial,SAR,Sentinel,Landsat
@@ -71,8 +71,6 @@ Requires-Dist: spyndex>=0.5; extra == "processor"
71
71
  Requires-Dist: scipy>=1.12; extra == "processor"
72
72
  Requires-Dist: scikit-image>=0.23; extra == "processor"
73
73
  Requires-Dist: geopandas>=0.14; extra == "processor"
74
- Provides-Extra: providers
75
- Requires-Dist: eodag>=3.0; extra == "providers"
76
74
  Provides-Extra: sar
77
75
  Requires-Dist: sarxarray>=0.1; extra == "sar"
78
76
  Provides-Extra: insar
@@ -130,12 +128,12 @@ Requires-Dist: mypy>=1.8; extra == "dev"
130
128
  Requires-Dist: build>=1.0; extra == "dev"
131
129
  Requires-Dist: twine>=5.0; extra == "dev"
132
130
  Provides-Extra: test
133
- Requires-Dist: pygeofetch[dev,geo,insar]; extra == "test"
131
+ Requires-Dist: pygeofetch[cloud,dev,geo,insar]; extra == "test"
134
132
  Provides-Extra: insar-full
135
133
  Requires-Dist: snaphu>=0.1; extra == "insar-full"
136
134
  Requires-Dist: scipy>=1.12; extra == "insar-full"
137
135
  Provides-Extra: full
138
- Requires-Dist: pygeofetch[cloud,geo,insar,notebook,processor,providers,schedule,viz]; extra == "full"
136
+ Requires-Dist: pygeofetch[cloud,geo,insar,notebook,processor,schedule,viz]; extra == "full"
139
137
  Provides-Extra: all
140
138
  Requires-Dist: pygeofetch[full]; extra == "all"
141
139
  Dynamic: license-file
@@ -153,9 +151,6 @@ Dynamic: license-file
153
151
 
154
152
  [Quick Start](#-quick-start) · [Mexico City Case Study](#-case-study-mapping-mexico-city-land-subsidence-with-insar) · [Case Study Repo ↗](https://github.com/EOCoreINT/mexico-subsidence-project) · [Documentation](https://pygeofetch.readthedocs.io/en/latest/) · [Notebooks](#-notebooks)
155
153
 
156
- <br>
157
- <img src="https://raw.githubusercontent.com/EOCoreINT/pygeofetch/refs/heads/main/icon/concept_a.png" alt="PyGeoFetch Logo" width="350">
158
- <br>
159
154
 
160
155
  </div>
161
156
 
@@ -386,7 +381,7 @@ If you use PyGeoFetch in your research or operational work, please cite it using
386
381
  title = {PyGeoFetch: A Unified Python Framework for Multi-Provider Satellite Data Acquisition, Pre-download Quality Control, and Geospatial Processing},
387
382
  year = {2026},
388
383
  publisher = {Zenodo},
389
- version = {2.6.2.3},
384
+ version = {2.6.2.5.1},
390
385
  doi = {10.5281/zenodo.22087230},
391
386
  url = {https://doi.org/10.5281/zenodo.22087230}
392
387
  }
@@ -11,9 +11,6 @@
11
11
 
12
12
  [Quick Start](#-quick-start) · [Mexico City Case Study](#-case-study-mapping-mexico-city-land-subsidence-with-insar) · [Case Study Repo ↗](https://github.com/EOCoreINT/mexico-subsidence-project) · [Documentation](https://pygeofetch.readthedocs.io/en/latest/) · [Notebooks](#-notebooks)
13
13
 
14
- <br>
15
- <img src="https://raw.githubusercontent.com/EOCoreINT/pygeofetch/refs/heads/main/icon/concept_a.png" alt="PyGeoFetch Logo" width="350">
16
- <br>
17
14
 
18
15
  </div>
19
16
 
@@ -244,7 +241,7 @@ If you use PyGeoFetch in your research or operational work, please cite it using
244
241
  title = {PyGeoFetch: A Unified Python Framework for Multi-Provider Satellite Data Acquisition, Pre-download Quality Control, and Geospatial Processing},
245
242
  year = {2026},
246
243
  publisher = {Zenodo},
247
- version = {2.6.2.3},
244
+ version = {2.6.2.5.1},
248
245
  doi = {10.5281/zenodo.22087230},
249
246
  url = {https://doi.org/10.5281/zenodo.22087230}
250
247
  }
@@ -30,7 +30,7 @@ CLI::
30
30
  pygeofetch doctor
31
31
  """
32
32
 
33
- __version__ = "2.6.2.3"
33
+ __version__ = "2.6.2.5.4"
34
34
  __author__ = "PyGeoFetch Contributors"
35
35
  __license__ = "MIT"
36
36
 
@@ -26,6 +26,8 @@ from pygeofetch.cli.auth_commands import auth
26
26
  from pygeofetch.cli.config_commands import config
27
27
  from pygeofetch.cli.download_commands import download
28
28
  from pygeofetch.cli.index_commands import index
29
+ from pygeofetch.cli.multisensor_commands import multisensor
30
+ from pygeofetch.cli.optical_commands import optical
29
31
  from pygeofetch.cli.pipeline_process_commands import proc_pipeline
30
32
  from pygeofetch.cli.postprocess_commands import post
31
33
  from pygeofetch.cli.preprocess_commands import preprocess
@@ -103,6 +105,8 @@ cli.add_command(index)
103
105
  cli.add_command(post)
104
106
  cli.add_command(sar)
105
107
  cli.add_command(proc_pipeline)
108
+ cli.add_command(optical)
109
+ cli.add_command(multisensor)
106
110
 
107
111
 
108
112
  # ---------------------------------------------------------------------------
@@ -0,0 +1,243 @@
1
+ """CLI access to pygeofetch's 5 real multi-sensor pipelines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import click
8
+
9
+
10
+ @click.group()
11
+ def multisensor() -> None:
12
+ """Multi-sensor fusion pipelines: InSAR+optical, SAR+optical flood
13
+ mapping, terrain-corrected change, DEM differencing, vegetation
14
+ disturbance. See docs/processing/multi-sensor-pipelines.md."""
15
+
16
+
17
+ def _print_result(result) -> None:
18
+ if result.success:
19
+ click.echo(f"✓ Success -> {result.final_output}")
20
+ if result.metadata:
21
+ click.echo(
22
+ json.dumps(
23
+ {k: str(v) for k, v in result.metadata.items()},
24
+ indent=2,
25
+ )
26
+ )
27
+ else:
28
+ click.echo(f"✗ Failed: {result.error}", err=True)
29
+ raise SystemExit(1)
30
+
31
+
32
+ @multisensor.command("insar-optical-fusion")
33
+ @click.option("--insar-velocity", required=True, type=click.Path(exists=True))
34
+ @click.option("--insar-coherence", required=True, type=click.Path(exists=True))
35
+ @click.option("--optical-ref", required=True, type=click.Path(exists=True))
36
+ @click.option("--optical-sec", required=True, type=click.Path(exists=True))
37
+ @click.option("--output-dir", required=True, type=click.Path())
38
+ @click.option("--window-size", default=64, show_default=True, type=int)
39
+ @click.option("--step-size", default=16, show_default=True, type=int)
40
+ @click.option("--snr-threshold", default=3.0, show_default=True, type=float)
41
+ @click.option("--coherence-low", default=0.4, show_default=True, type=float)
42
+ @click.option("--coherence-high", default=0.6, show_default=True, type=float)
43
+ @click.option("--cloud-mask", default=None, type=click.Path(exists=True))
44
+ def insar_optical_fusion_cmd(
45
+ insar_velocity,
46
+ insar_coherence,
47
+ optical_ref,
48
+ optical_sec,
49
+ output_dir,
50
+ window_size,
51
+ step_size,
52
+ snr_threshold,
53
+ coherence_low,
54
+ coherence_high,
55
+ cloud_mask,
56
+ ):
57
+ """Fuse InSAR displacement with optical pixel offset tracking."""
58
+ from pygeofetch.multisensor import insar_optical_displacement_pipeline
59
+
60
+ result = insar_optical_displacement_pipeline(
61
+ insar_velocity,
62
+ insar_coherence,
63
+ optical_ref,
64
+ optical_sec,
65
+ output_dir,
66
+ cloud_mask_path=cloud_mask,
67
+ window_size=window_size,
68
+ step_size=step_size,
69
+ snr_threshold=snr_threshold,
70
+ coherence_low=coherence_low,
71
+ coherence_high=coherence_high,
72
+ )
73
+ _print_result(result)
74
+
75
+
76
+ @multisensor.command("flood-map")
77
+ @click.option("--sar", "sar_input", required=True, type=click.Path(exists=True))
78
+ @click.option("--optical-green", required=True, type=click.Path(exists=True))
79
+ @click.option("--optical-nir", required=True, type=click.Path(exists=True))
80
+ @click.option("--output-dir", required=True, type=click.Path())
81
+ @click.option("--sar-reference", default=None, type=click.Path(exists=True))
82
+ @click.option("--sar-threshold", default=-15.0, show_default=True, type=float)
83
+ @click.option("--ndwi-threshold", default=0.0, show_default=True, type=float)
84
+ @click.option("--cloud-mask", default=None, type=click.Path(exists=True))
85
+ @click.option(
86
+ "--fusion-mode",
87
+ default="cloud_aware",
88
+ show_default=True,
89
+ type=click.Choice(["cloud_aware", "union", "intersection"]),
90
+ )
91
+ def flood_map_cmd(
92
+ sar_input,
93
+ optical_green,
94
+ optical_nir,
95
+ output_dir,
96
+ sar_reference,
97
+ sar_threshold,
98
+ ndwi_threshold,
99
+ cloud_mask,
100
+ fusion_mode,
101
+ ):
102
+ """Fuse SAR and optical NDWI for flood/water extent mapping."""
103
+ from pygeofetch.multisensor import multi_sensor_flood_pipeline
104
+ from pygeofetch.sar import SARProcessor
105
+
106
+ result = multi_sensor_flood_pipeline(
107
+ SARProcessor(),
108
+ sar_input,
109
+ optical_green,
110
+ optical_nir,
111
+ output_dir,
112
+ sar_reference_path=sar_reference,
113
+ sar_threshold=sar_threshold,
114
+ optical_ndwi_threshold=ndwi_threshold,
115
+ cloud_mask_path=cloud_mask,
116
+ fusion_mode=fusion_mode,
117
+ )
118
+ _print_result(result)
119
+
120
+
121
+ @multisensor.command("terrain-change")
122
+ @click.option("--pre", "pre_path", required=True, type=click.Path(exists=True))
123
+ @click.option("--post", "post_path", required=True, type=click.Path(exists=True))
124
+ @click.option("--dem", "dem_path", required=True, type=click.Path(exists=True))
125
+ @click.option(
126
+ "--pre-datetime",
127
+ required=True,
128
+ help="Real UTC ISO datetime, e.g. 2024-01-15T10:30:00",
129
+ )
130
+ @click.option("--post-datetime", required=True, help="Real UTC ISO datetime")
131
+ @click.option("--lat", "latitude", required=True, type=float)
132
+ @click.option("--lon", "longitude", required=True, type=float)
133
+ @click.option("--output-dir", required=True, type=click.Path())
134
+ @click.option("--change-threshold", default=0.1, show_default=True, type=float)
135
+ def terrain_change_cmd(
136
+ pre_path,
137
+ post_path,
138
+ dem_path,
139
+ pre_datetime,
140
+ post_datetime,
141
+ latitude,
142
+ longitude,
143
+ output_dir,
144
+ change_threshold,
145
+ ):
146
+ """Terrain-corrected optical change detection (removes real
147
+ illumination artifacts using each date's own solar position)."""
148
+ from datetime import datetime, timezone
149
+
150
+ from pygeofetch.multisensor import terrain_corrected_change_pipeline
151
+
152
+ def _parse(dt_str):
153
+ dt = datetime.fromisoformat(dt_str)
154
+ return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
155
+
156
+ result = terrain_corrected_change_pipeline(
157
+ pre_path,
158
+ post_path,
159
+ dem_path,
160
+ _parse(pre_datetime),
161
+ _parse(post_datetime),
162
+ latitude,
163
+ longitude,
164
+ output_dir,
165
+ change_threshold=change_threshold,
166
+ )
167
+ _print_result(result)
168
+
169
+
170
+ @multisensor.command("dem-diff")
171
+ @click.option("--dem-old", required=True, type=click.Path(exists=True))
172
+ @click.option("--dem-new", required=True, type=click.Path(exists=True))
173
+ @click.option("--output-dir", required=True, type=click.Path())
174
+ @click.option(
175
+ "--old-rmse",
176
+ default=1.0,
177
+ show_default=True,
178
+ type=float,
179
+ help="Real vertical RMSE of the old DEM, metres.",
180
+ )
181
+ @click.option(
182
+ "--new-rmse",
183
+ default=1.0,
184
+ show_default=True,
185
+ type=float,
186
+ help="Real vertical RMSE of the new DEM, metres.",
187
+ )
188
+ @click.option("--confidence-t", default=1.96, show_default=True, type=float)
189
+ def dem_diff_cmd(dem_old, dem_new, output_dir, old_rmse, new_rmse, confidence_t):
190
+ """DEM-of-Difference volumetric change analysis."""
191
+ from pygeofetch.multisensor import dem_differencing_pipeline
192
+
193
+ result = dem_differencing_pipeline(
194
+ dem_old,
195
+ dem_new,
196
+ output_dir,
197
+ dem_old_vertical_rmse_m=old_rmse,
198
+ dem_new_vertical_rmse_m=new_rmse,
199
+ confidence_t_value=confidence_t,
200
+ )
201
+ _print_result(result)
202
+
203
+
204
+ @multisensor.command("vegetation-disturbance")
205
+ @click.option("--red-pre", required=True, type=click.Path(exists=True))
206
+ @click.option("--nir-pre", required=True, type=click.Path(exists=True))
207
+ @click.option("--red-post", required=True, type=click.Path(exists=True))
208
+ @click.option("--nir-post", required=True, type=click.Path(exists=True))
209
+ @click.option("--slc-pre", required=True, type=click.Path(exists=True))
210
+ @click.option("--slc-post", required=True, type=click.Path(exists=True))
211
+ @click.option("--output-dir", required=True, type=click.Path())
212
+ @click.option("--ndvi-drop-threshold", default=0.15, show_default=True, type=float)
213
+ @click.option("--coherence-threshold", default=0.3, show_default=True, type=float)
214
+ @click.option("--coherence-window", default=7, show_default=True, type=int)
215
+ def vegetation_disturbance_cmd(
216
+ red_pre,
217
+ nir_pre,
218
+ red_post,
219
+ nir_post,
220
+ slc_pre,
221
+ slc_post,
222
+ output_dir,
223
+ ndvi_drop_threshold,
224
+ coherence_threshold,
225
+ coherence_window,
226
+ ):
227
+ """Classify visible canopy loss vs. real sub-canopy ground
228
+ disturbance, from optical NDVI + SAR coherence."""
229
+ from pygeofetch.multisensor import vegetation_disturbance_pipeline
230
+
231
+ result = vegetation_disturbance_pipeline(
232
+ red_pre,
233
+ nir_pre,
234
+ red_post,
235
+ nir_post,
236
+ slc_pre,
237
+ slc_post,
238
+ output_dir,
239
+ ndvi_drop_threshold=ndvi_drop_threshold,
240
+ coherence_threshold=coherence_threshold,
241
+ coherence_window=coherence_window,
242
+ )
243
+ _print_result(result)
@@ -0,0 +1,105 @@
1
+ """CLI commands for optical pixel offset tracking (POT)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.group()
14
+ def optical() -> None:
15
+ """Optical pixel offset tracking — measures large 2D ground
16
+ displacement via image correlation, complementing InSAR for
17
+ motion that exceeds phase InSAR's coherence limit."""
18
+
19
+
20
+ @optical.command("offset-track")
21
+ @click.option("--ref", "reference_path", required=True, type=click.Path(exists=True), help="Reference-date raster.")
22
+ @click.option("--sec", "secondary_path", required=True, type=click.Path(exists=True), help="Secondary-date raster.")
23
+ @click.option("--output", "-o", required=True, type=click.Path(), help="Output GeoTIFF (dx/dy/snr, 3 bands).")
24
+ @click.option("--band", default=1, show_default=True, type=int, help="1-based band index to correlate.")
25
+ @click.option("--window-size", default=64, show_default=True, type=int, help="Search window size, pixels.")
26
+ @click.option("--step-size", default=16, show_default=True, type=int, help="Spacing between window centers, pixels.")
27
+ @click.option("--chip-size", default=None, type=int, help="Reference template size, pixels. Default: window-size // 2.")
28
+ @click.option("--snr-threshold", default=3.0, show_default=True, type=float, help="Minimum SNR to mark a window reliable.")
29
+ @click.option("--cloud-mask", default=None, type=click.Path(exists=True), help="Real Sentinel-2 SCL raster, already aligned to --ref.")
30
+ @click.option("--water-mask-threshold", default=None, type=float, help="Mask pixels where NDWI exceeds this value (needs --green-band/--nir-band).")
31
+ @click.option("--green-band", default=None, type=int, help="1-based green band index, for --water-mask-threshold.")
32
+ @click.option("--nir-band", default=None, type=int, help="1-based NIR band index, for --water-mask-threshold.")
33
+ def offset_track_cmd(
34
+ reference_path,
35
+ secondary_path,
36
+ output,
37
+ band,
38
+ window_size,
39
+ step_size,
40
+ chip_size,
41
+ snr_threshold,
42
+ cloud_mask,
43
+ water_mask_threshold,
44
+ green_band,
45
+ nir_band,
46
+ ):
47
+ """
48
+ Measure 2D ground displacement between two optical scenes via
49
+ image correlation.
50
+
51
+ \b
52
+ Examples:
53
+ # Basic: two co-registered scenes, real cloud masking via SCL
54
+ pygeofetch optical offset-track \\
55
+ --ref before.tif --sec after.tif \\
56
+ --cloud-mask before_SCL.tif \\
57
+ --output displacement.tif
58
+
59
+ # Mining-scale displacement, larger search window
60
+ pygeofetch optical offset-track \\
61
+ --ref jan.tif --sec jun.tif \\
62
+ --window-size 96 --step-size 24 \\
63
+ --output mine_displacement.tif
64
+ """
65
+ from pygeofetch.optical.offset_tracking import compute_pixel_offsets, prepare_optical_pair
66
+
67
+ try:
68
+ ref_arr, sec_arr, ref_profile = prepare_optical_pair(
69
+ reference_path,
70
+ secondary_path,
71
+ band_index=band,
72
+ cloud_mask_path=cloud_mask,
73
+ water_mask_threshold=water_mask_threshold,
74
+ green_band_index=green_band,
75
+ nir_band_index=nir_band,
76
+ )
77
+ except ValueError as exc:
78
+ console.print(f"[red]✗[/] {exc}")
79
+ sys.exit(1)
80
+
81
+ pixel_size_m = abs(ref_profile["transform"].a)
82
+ result = compute_pixel_offsets(
83
+ ref_arr,
84
+ sec_arr,
85
+ pixel_size_m=pixel_size_m,
86
+ window_size=window_size,
87
+ step_size=step_size,
88
+ chip_size=chip_size,
89
+ snr_threshold=snr_threshold,
90
+ )
91
+
92
+ out_transform = ref_profile["transform"] * ref_profile["transform"].scale(step_size, step_size)
93
+ out_profile = dict(ref_profile)
94
+ out_profile.update(
95
+ height=result.dx.shape[0],
96
+ width=result.dx.shape[1],
97
+ transform=out_transform,
98
+ )
99
+ written = result.export_geotiff(output, out_profile)
100
+
101
+ n_reliable = int(result.reliable.sum())
102
+ n_total = int(result.reliable.size)
103
+ console.print(f"[green]✓[/] offset-track → {written}")
104
+ console.print(f" {n_reliable}/{n_total} windows reliable ({100 * n_reliable / n_total:.1f}%)")
105
+ console.print(f" window={window_size}px step={step_size}px pixel_size={pixel_size_m:.2f}m")
@@ -282,6 +282,7 @@ def search_run(
282
282
 
283
283
  sb = PyGeoFetch()
284
284
 
285
+ optical_config = None
285
286
  if validate_optical and (
286
287
  optical_max_cloud_cover,
287
288
  optical_min_coverage,
@@ -298,22 +299,48 @@ def search_run(
298
299
  overrides["required_bands"] = [
299
300
  b.strip() for b in optical_required_bands.split(",")
300
301
  ]
301
- sb.optical_validation_config = OpticalValidationConfig(**overrides)
302
+ optical_config = OpticalValidationConfig(**overrides)
303
+ sb.optical_validation_config = optical_config
302
304
 
303
305
  with console.status(
304
306
  f"[cyan]Searching {len(provider_list or [])} provider(s)...[/]"
305
307
  ):
306
- results = sb.search(
307
- query,
308
- providers=provider_list,
309
- use_cache=not no_cache,
310
- validate_optical=validate_optical,
311
- )
308
+ if validate_optical:
309
+ # Real, deliberate choice: search WITHOUT filtering here, then
310
+ # validate directly in this CLI layer, rather than calling
311
+ # search(validate_optical=True) and losing visibility into
312
+ # what got rejected and why. sb.search()'s own contract
313
+ # (returns only the passed scenes) is left untouched for
314
+ # every other caller -- this is a CLI-only reporting path,
315
+ # not a change to the library's real filtering behavior.
316
+ raw_results = sb.search(
317
+ query,
318
+ providers=provider_list,
319
+ use_cache=not no_cache,
320
+ validate_optical=False,
321
+ )
322
+ results, validation_summary = _validate_and_summarize(
323
+ sb, raw_results, query, optical_config
324
+ )
325
+ else:
326
+ results = sb.search(
327
+ query,
328
+ providers=provider_list,
329
+ use_cache=not no_cache,
330
+ validate_optical=False,
331
+ )
332
+ validation_summary = None
312
333
 
313
334
  if not results:
314
335
  console.print("[yellow]No results found.[/]")
315
336
  return
316
337
 
338
+ # Real, clean validation info + full results table, organized so the
339
+ # summary always appears before the detailed table -- not interleaved
340
+ # with save/format output below.
341
+ if validation_summary is not None:
342
+ _display_validation_summary(validation_summary)
343
+
317
344
  # Save if requested
318
345
  if output:
319
346
  out_path = Path(output)
@@ -365,6 +392,92 @@ def search_run(
365
392
  click.echo(r.id)
366
393
 
367
394
 
395
+ def _validate_and_summarize(sb, raw_results, query, optical_config):
396
+ """
397
+ Real validation pass over every raw candidate, returning both the
398
+ scenes that actually pass and a real, structured summary of what got
399
+ rejected and why -- the information sb.search(validate_optical=True)
400
+ alone discards once it returns only the filtered list.
401
+ """
402
+ from collections import Counter
403
+
404
+ from pygeofetch.validation import OpticalPreflightValidator, OpticalValidationConfig
405
+
406
+ validator = OpticalPreflightValidator(optical_config or OpticalValidationConfig())
407
+ aoi = sb._derive_aoi_polygon(query)
408
+
409
+ passed_results = []
410
+ rejection_reasons: Counter = Counter()
411
+ warning_reasons: Counter = Counter()
412
+ n_warned_but_passed = 0
413
+
414
+ for scene in raw_results:
415
+ report = validator.validate_scene(scene, aoi, query.start_date, query.end_date)
416
+ if report.passed:
417
+ passed_results.append(scene)
418
+ if report.issues:
419
+ n_warned_but_passed += 1
420
+ for issue in report.issues:
421
+ warning_reasons[issue.code] += 1
422
+ else:
423
+ for issue in report.issues:
424
+ if issue.severity == "ERROR":
425
+ rejection_reasons[issue.code] += 1
426
+
427
+ summary = {
428
+ "n_total": len(raw_results),
429
+ "n_passed": len(passed_results),
430
+ "n_rejected": len(raw_results) - len(passed_results),
431
+ "n_warned_but_passed": n_warned_but_passed,
432
+ "rejection_reasons": rejection_reasons,
433
+ "warning_reasons": warning_reasons,
434
+ }
435
+ return passed_results, summary
436
+
437
+
438
+ def _display_validation_summary(summary: dict) -> None:
439
+ """Clean, real info block summarizing optical validation -- shown
440
+ before the detailed results table, not interleaved with it."""
441
+ n_total = summary["n_total"]
442
+ n_passed = summary["n_passed"]
443
+ n_rejected = summary["n_rejected"]
444
+ pct = (100 * n_passed / n_total) if n_total else 0.0
445
+
446
+ console.print(
447
+ f"\n[bold]Optical validation:[/] {n_passed}/{n_total} scenes passed "
448
+ f"({pct:.0f}%)"
449
+ + (f", [yellow]{n_rejected} rejected[/]" if n_rejected else "")
450
+ + (
451
+ f", [cyan]{summary['n_warned_but_passed']} passed with warnings[/]"
452
+ if summary["n_warned_but_passed"]
453
+ else ""
454
+ )
455
+ )
456
+
457
+ if summary["rejection_reasons"]:
458
+ reasons_table = Table(
459
+ title="Rejection reasons", header_style="bold red", show_lines=False
460
+ )
461
+ reasons_table.add_column("Code", style="red")
462
+ reasons_table.add_column("Count", justify="right")
463
+ for code, count in summary["rejection_reasons"].most_common():
464
+ reasons_table.add_row(code, str(count))
465
+ console.print(reasons_table)
466
+
467
+ if summary["warning_reasons"]:
468
+ warn_table = Table(
469
+ title="Warnings on passed scenes",
470
+ header_style="bold yellow",
471
+ show_lines=False,
472
+ )
473
+ warn_table.add_column("Code", style="yellow")
474
+ warn_table.add_column("Count", justify="right")
475
+ for code, count in summary["warning_reasons"].most_common():
476
+ warn_table.add_row(code, str(count))
477
+ console.print(warn_table)
478
+ console.print() # real spacing before the full results table below
479
+
480
+
368
481
  def _display_table(results) -> None:
369
482
  table = Table(title=f"{len(results)} Results", header_style="bold blue")
370
483
  table.add_column("ID", style="cyan", max_width=30)
@@ -441,4 +554,4 @@ def _save_csv(results, path: Path) -> None:
441
554
  )
442
555
  writer.writeheader()
443
556
  for r in results:
444
- writer.writerow(_item_to_dict(r))
557
+ writer.writerow(_item_to_dict(r))