PyGeoFetch 2.6.2__py3-none-any.whl → 2.6.2.1__py3-none-any.whl

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 (82) hide show
  1. pygeofetch/__init__.py +1 -1
  2. pygeofetch/cli/auth_commands.py +6 -3
  3. pygeofetch/cli/main.py +10 -6
  4. pygeofetch/cli/monitor_commands.py +49 -14
  5. pygeofetch/cli/search_commands.py +1 -1
  6. pygeofetch/core/authenticator.py +5 -3
  7. pygeofetch/core/downloader.py +31 -13
  8. pygeofetch/core/engine.py +1 -1
  9. pygeofetch/core/monitor.py +16 -5
  10. pygeofetch/core/state.py +20 -6
  11. pygeofetch/insar/__init__.py +96 -71
  12. pygeofetch/insar/analysis.py +62 -40
  13. pygeofetch/insar/annotation.py +116 -70
  14. pygeofetch/insar/atmosphere.py +168 -70
  15. pygeofetch/insar/coregister.py +126 -58
  16. pygeofetch/insar/deburst.py +39 -12
  17. pygeofetch/insar/esd.py +65 -28
  18. pygeofetch/insar/extraction.py +126 -67
  19. pygeofetch/insar/flatearth.py +45 -18
  20. pygeofetch/insar/geolocation.py +79 -29
  21. pygeofetch/insar/gpu.py +3 -1
  22. pygeofetch/insar/interferogram.py +502 -193
  23. pygeofetch/insar/ionosphere.py +118 -45
  24. pygeofetch/insar/offset_geometry.py +47 -43
  25. pygeofetch/insar/offset_tracking.py +45 -20
  26. pygeofetch/insar/preflight.py +288 -169
  27. pygeofetch/insar/provenance.py +2 -1
  28. pygeofetch/insar/ps_selection.py +21 -9
  29. pygeofetch/insar/splitspectrum_diagnostic.py +12 -4
  30. pygeofetch/insar/stack_selection.py +179 -77
  31. pygeofetch/insar/synthetic.py +53 -17
  32. pygeofetch/insar/timeseries.py +268 -152
  33. pygeofetch/insar/unwrap.py +408 -156
  34. pygeofetch/insar/validate.py +36 -13
  35. pygeofetch/insar/visualize.py +15 -5
  36. pygeofetch/insar/workflow.py +102 -29
  37. pygeofetch/meta.yaml +1 -1
  38. pygeofetch/models/satellite_data.py +5 -3
  39. pygeofetch/processing/indices.py +5 -0
  40. pygeofetch/processing/preprocessor.py +55 -25
  41. pygeofetch/processing/sar.py +13 -4
  42. pygeofetch/processor/indices.py +4 -0
  43. pygeofetch/processor/stacker.py +4 -0
  44. pygeofetch/processor/timeseries.py +41 -13
  45. pygeofetch/providers/airbus_oneatlas.py +17 -4
  46. pygeofetch/providers/alaska_satellite_facility.py +5 -2
  47. pygeofetch/providers/aws_earth.py +1 -1
  48. pygeofetch/providers/copernicus.py +19 -8
  49. pygeofetch/providers/copernicus_nodes.py +73 -23
  50. pygeofetch/providers/digitalglobe.py +5 -2
  51. pygeofetch/providers/earth_explorer_additional.py +5 -2
  52. pygeofetch/providers/element84.py +1 -1
  53. pygeofetch/providers/eodag_provider.py +9 -5
  54. pygeofetch/providers/esa_scihub.py +17 -4
  55. pygeofetch/providers/geoserver_generic.py +5 -2
  56. pygeofetch/providers/google_earth_engine.py +17 -4
  57. pygeofetch/providers/inpe_cbers.py +5 -2
  58. pygeofetch/providers/isro_bhuvan.py +5 -2
  59. pygeofetch/providers/jaxa_earth.py +5 -2
  60. pygeofetch/providers/maxar_gbdx.py +5 -2
  61. pygeofetch/providers/nasa_earthdata.py +6 -4
  62. pygeofetch/providers/nasa_earthdata_cloud.py +2 -2
  63. pygeofetch/providers/noaa_big_data.py +5 -2
  64. pygeofetch/providers/opentopography.py +6 -2
  65. pygeofetch/providers/planet.py +4 -3
  66. pygeofetch/providers/planetary_computer.py +1 -1
  67. pygeofetch/providers/sentinel_hub.py +11 -7
  68. pygeofetch/providers/terrabotics.py +4 -3
  69. pygeofetch/providers/usgs.py +20 -5
  70. pygeofetch/sar/__init__.py +1 -1
  71. pygeofetch/sar/_native.py +1 -1
  72. pygeofetch/sar/extraction.py +8 -4
  73. pygeofetch/utils/gcp_recovery.py +1 -3
  74. pygeofetch/viz/map.py +82 -33
  75. pygeofetch/viz/plot.py +91 -38
  76. {pygeofetch-2.6.2.dist-info → pygeofetch-2.6.2.1.dist-info}/METADATA +25 -28
  77. pygeofetch-2.6.2.1.dist-info/RECORD +122 -0
  78. pygeofetch-2.6.2.dist-info/RECORD +0 -122
  79. {pygeofetch-2.6.2.dist-info → pygeofetch-2.6.2.1.dist-info}/WHEEL +0 -0
  80. {pygeofetch-2.6.2.dist-info → pygeofetch-2.6.2.1.dist-info}/entry_points.txt +0 -0
  81. {pygeofetch-2.6.2.dist-info → pygeofetch-2.6.2.1.dist-info}/licenses/LICENSE +0 -0
  82. {pygeofetch-2.6.2.dist-info → pygeofetch-2.6.2.1.dist-info}/top_level.txt +0 -0
pygeofetch/__init__.py CHANGED
@@ -30,7 +30,7 @@ CLI::
30
30
  pygeofetch doctor
31
31
  """
32
32
 
33
- __version__ = "2.6.2"
33
+ __version__ = "2.6.2.1"
34
34
  __author__ = "PyGeoFetch Contributors"
35
35
  __license__ = "MIT"
36
36
 
@@ -221,8 +221,11 @@ def auth_remove(provider: str, yes: bool) -> None:
221
221
  click.confirm(f"Remove credentials for {provider!r}?", abort=True)
222
222
 
223
223
  mgr = AuthManager()
224
- mgr.remove_credentials(provider)
225
- console.print(f"[green]Credentials for [bold]{provider}[/] removed.[/]")
224
+ removed = mgr.remove(provider)
225
+ if removed:
226
+ console.print(f"[green]Credentials for [bold]{provider}[/] removed.[/]")
227
+ else:
228
+ console.print(f"[yellow]No stored credentials found for {provider!r}.[/]")
226
229
 
227
230
 
228
231
  @auth.command(name="export")
@@ -250,7 +253,7 @@ def auth_export(provider: str | None, output: str) -> None:
250
253
  from pygeofetch.core.authenticator import AuthManager
251
254
 
252
255
  mgr = AuthManager()
253
- data = mgr.export_credentials(provider_filter=provider)
256
+ data = mgr.export_credentials(providers=[provider] if provider else None)
254
257
 
255
258
  with open(output, "w") as f:
256
259
  json.dump(data, f, indent=2, default=str)
pygeofetch/cli/main.py CHANGED
@@ -484,9 +484,11 @@ def providers_info(provider_id: str, as_json: bool) -> None:
484
484
  table.add_row("Auth type", caps.auth_type)
485
485
  table.add_row(
486
486
  "Auth status",
487
- f"[green]{auth_status}[/]"
488
- if "✓" in auth_status or "🌐" in auth_status
489
- else f"[red]{auth_status}[/]",
487
+ (
488
+ f"[green]{auth_status}[/]"
489
+ if "✓" in auth_status or "🌐" in auth_status
490
+ else f"[red]{auth_status}[/]"
491
+ ),
490
492
  )
491
493
  table.add_row("Satellites", ", ".join(caps.satellites) if caps.satellites else "—")
492
494
  table.add_row("SAR support", "[green]Yes[/]" if caps.supports_sar else "No")
@@ -495,9 +497,11 @@ def providers_info(provider_id: str, as_json: bool) -> None:
495
497
  table.add_row("CQL2 filter", "[green]Yes[/]" if caps.supports_cql2 else "No")
496
498
  table.add_row(
497
499
  "Resolution",
498
- f"{caps.resolution_min_m}m – {caps.resolution_max_m}m"
499
- if caps.resolution_min_m
500
- else "Varies",
500
+ (
501
+ f"{caps.resolution_min_m}m – {caps.resolution_max_m}m"
502
+ if caps.resolution_min_m
503
+ else "Varies"
504
+ ),
501
505
  )
502
506
  table.add_row("Regions", ", ".join(caps.regions) if caps.regions else "global")
503
507
  table.add_row("Endpoint", caps.endpoint_url or "—")
@@ -33,22 +33,51 @@ console = Console()
33
33
  logger = logging.getLogger("pygeofetch.cli.monitor")
34
34
 
35
35
 
36
- @click.group(name="monitor", help="Automated, incremental InSAR monitoring (Feature 4).")
36
+ @click.group(
37
+ name="monitor", help="Automated, incremental InSAR monitoring (Feature 4)."
38
+ )
37
39
  def monitor():
38
40
  pass
39
41
 
40
42
 
41
- @monitor.command(name="run", help="Run one real monitoring cycle: search for new scenes, plan incremental processing.")
42
- @click.option("--state-db", required=True, type=click.Path(), help="Path to this project's real state database.")
43
- @click.option("--bbox", required=True, type=str, help="Real AOI bounding box, 'minlon,minlat,maxlon,maxlat'.")
44
- @click.option("--satellites", default="Sentinel-1A,Sentinel-1B", help="Comma-separated real satellite names.")
45
- @click.option("--n-neighbors", default=3, type=int, help="Real neighbors each new date connects to (spec default: 3).")
46
- @click.option("--cron-daily", is_flag=True, help="Marker flag for scheduled invocation (real cron job); does not change behavior, documents intent in logs.")
43
+ @monitor.command(
44
+ name="run",
45
+ help="Run one real monitoring cycle: search for new scenes, plan incremental processing.",
46
+ )
47
+ @click.option(
48
+ "--state-db",
49
+ required=True,
50
+ type=click.Path(),
51
+ help="Path to this project's real state database.",
52
+ )
53
+ @click.option(
54
+ "--bbox",
55
+ required=True,
56
+ type=str,
57
+ help="Real AOI bounding box, 'minlon,minlat,maxlon,maxlat'.",
58
+ )
59
+ @click.option(
60
+ "--satellites",
61
+ default="Sentinel-1A,Sentinel-1B",
62
+ help="Comma-separated real satellite names.",
63
+ )
64
+ @click.option(
65
+ "--n-neighbors",
66
+ default=3,
67
+ type=int,
68
+ help="Real neighbors each new date connects to (spec default: 3).",
69
+ )
70
+ @click.option(
71
+ "--cron-daily",
72
+ is_flag=True,
73
+ help="Marker flag for scheduled invocation (real cron job); does not change behavior, documents intent in logs.",
74
+ )
47
75
  @click.pass_context
48
76
  def monitor_run(ctx, state_db, bbox, satellites, n_neighbors, cron_daily):
49
77
  client = ctx.obj.get("client") if ctx.obj else None
50
78
  if client is None:
51
79
  from pygeofetch import PyGeoFetch
80
+
52
81
  client = PyGeoFetch()
53
82
 
54
83
  state = ProjectState(state_db)
@@ -66,7 +95,9 @@ def monitor_run(ctx, state_db, bbox, satellites, n_neighbors, cron_daily):
66
95
  if state.last_download_timestamp:
67
96
  query.start_date = state.last_download_timestamp
68
97
 
69
- console.print(f"[bold]pygeofetch monitor[/bold] — {'scheduled' if cron_daily else 'manual'} run")
98
+ console.print(
99
+ f"[bold]pygeofetch monitor[/bold] — {'scheduled' if cron_daily else 'manual'} run"
100
+ )
70
101
  console.print(f"State: {state_db}")
71
102
 
72
103
  try:
@@ -76,7 +107,9 @@ def monitor_run(ctx, state_db, bbox, satellites, n_neighbors, cron_daily):
76
107
  console.print(f"[red]Search failed:[/red] {exc}")
77
108
  raise
78
109
 
79
- available_dates = sorted({str(r.datetime)[:10] for r in results if getattr(r, "datetime", None)})
110
+ available_dates = sorted(
111
+ {str(r.datetime)[:10] for r in results if getattr(r, "datetime", None)}
112
+ )
80
113
  plan = plan_monitoring_run(state, available_dates, n_neighbors=n_neighbors)
81
114
 
82
115
  console.print(plan.message)
@@ -84,11 +117,13 @@ def monitor_run(ctx, state_db, bbox, satellites, n_neighbors, cron_daily):
84
117
  console.print(f" New dates: {plan.new_dates}")
85
118
  console.print(f" New pairs needed: {len(plan.new_pairs)}")
86
119
 
87
- state.record_run(RunSummary(
88
- status="success" if plan.network_changed else "no_new_scenes",
89
- n_new_scenes=len(plan.new_dates),
90
- detail=plan.message,
91
- ))
120
+ state.record_run(
121
+ RunSummary(
122
+ status="success" if plan.network_changed else "no_new_scenes",
123
+ n_new_scenes=len(plan.new_dates),
124
+ detail=plan.message,
125
+ )
126
+ )
92
127
 
93
128
  return plan
94
129
 
@@ -67,7 +67,7 @@ def search() -> None:
67
67
  @click.option(
68
68
  "--polarisation",
69
69
  default=None,
70
- help='SAR polarisation e.g. VV, VH, HH, HV. Same Copernicus-only caveat as --product-type.',
70
+ help="SAR polarisation e.g. VV, VH, HH, HV. Same Copernicus-only caveat as --product-type.",
71
71
  )
72
72
  @click.option("--max-results", "-n", default=100, show_default=True)
73
73
  @click.option(
@@ -447,9 +447,11 @@ class AuthManager:
447
447
  {
448
448
  "provider": provider,
449
449
  "has_session": session is not None and session.is_valid,
450
- "session_expires": session.expires_at.isoformat()
451
- if session and session.expires_at
452
- else None,
450
+ "session_expires": (
451
+ session.expires_at.isoformat()
452
+ if session and session.expires_at
453
+ else None
454
+ ),
453
455
  }
454
456
  )
455
457
  return result
@@ -25,7 +25,7 @@ import hashlib
25
25
  import time
26
26
  from concurrent.futures import ThreadPoolExecutor, as_completed
27
27
  from pathlib import Path
28
- from typing import TYPE_CHECKING, Any, Callable, Dict
28
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, cast
29
29
 
30
30
  from pygeofetch.core.logging import DownloadProgress, get_logger
31
31
  from pygeofetch.models.download_task import (
@@ -365,16 +365,19 @@ class AdaptiveDownloader:
365
365
  sum(1 for r in results if r and r.success)
366
366
  sum(1 for r in results if r and not r.success)
367
367
  dp.__exit__(None, None, None)
368
- assert len(results) == len(data_list), (
369
- f"BUG: result count {len(results)} != input count {len(data_list)}"
370
- )
368
+ assert len(results) == len(
369
+ data_list
370
+ ), f"BUG: result count {len(results)} != input count {len(data_list)}"
371
371
  sum(
372
372
  r.bytes_downloaded / (1024 * 1024)
373
373
  for r in results
374
374
  if r and r.success and r.bytes_downloaded
375
375
  )
376
376
  # summary printed by DownloadProgress footer
377
- return results
377
+ # The loop above has already replaced every None slot with a
378
+ # real DownloadResult (failure placeholder), so this is
379
+ # genuinely list[DownloadResult] at this point.
380
+ return cast(List[DownloadResult], results)
378
381
 
379
382
  def _get_provider(self, provider_id: str) -> Any:
380
383
  """Get a provider instance with a fresh authenticated session."""
@@ -889,7 +892,7 @@ class AdaptiveDownloader:
889
892
  from pygeofetch.processor import SpectralIndex
890
893
  except ImportError as exc:
891
894
  raise ImportError(
892
- f'{index_name} requires the processor extra: '
895
+ f"{index_name} requires the processor extra: "
893
896
  f'pip install "pygeofetch[processor]"'
894
897
  ) from exc
895
898
 
@@ -904,7 +907,10 @@ class AdaptiveDownloader:
904
907
  (
905
908
  p
906
909
  for p in result.output_paths
907
- if any(pat in p.name or pat.lower() in p.name.lower() for pat in patterns)
910
+ if any(
911
+ pat in p.name or pat.lower() in p.name.lower()
912
+ for pat in patterns
913
+ )
908
914
  ),
909
915
  None,
910
916
  )
@@ -914,8 +920,15 @@ class AdaptiveDownloader:
914
920
  f"{band_name} (looked for {patterns} in filenames)"
915
921
  )
916
922
  with rasterio.open(path) as src:
917
- arr = src.read(1, out_shape=ref_shape, resampling=rasterio.enums.Resampling.bilinear) \
918
- if ref_shape else src.read(1)
923
+ arr = (
924
+ src.read(
925
+ 1,
926
+ out_shape=ref_shape,
927
+ resampling=rasterio.enums.Resampling.bilinear,
928
+ )
929
+ if ref_shape
930
+ else src.read(1)
931
+ )
919
932
  if ref_shape is None:
920
933
  ref_shape = arr.shape
921
934
  profile = src.profile.copy()
@@ -929,6 +942,12 @@ class AdaptiveDownloader:
929
942
  arr = np.where(arr == src.nodata, np.nan, arr)
930
943
  band_arrays[band_name] = arr
931
944
 
945
+ assert ref_shape is not None and profile is not None, (
946
+ f"{index_name}: required_bands was empty or the loop never "
947
+ f"ran -- ref_shape/profile must be set by the first band "
948
+ f"processed above."
949
+ )
950
+
932
951
  si = SpectralIndex(prefer_spyndex=False)
933
952
  index_arr = np.asarray(si.compute(index_name, **band_arrays), dtype=np.float32)
934
953
  # Any band being NaN at a pixel must make the index NaN there too,
@@ -950,7 +969,6 @@ class AdaptiveDownloader:
950
969
  logger.info(f"{index_name} computed → {out_path}")
951
970
  return result
952
971
 
953
-
954
972
  def _find_existing_download(
955
973
  self, data: "SatelliteData", destination: Path
956
974
  ) -> Path | None:
@@ -1230,7 +1248,8 @@ class AdaptiveDownloader:
1230
1248
  "%s has no direct CRS/transform, using its %d "
1231
1249
  "ground control points for reprojection instead "
1232
1250
  "(common for raw SAR/GRD delivery).",
1233
- source_path.name, len(src_gcps),
1251
+ source_path.name,
1252
+ len(src_gcps),
1234
1253
  )
1235
1254
  elif src_crs is None:
1236
1255
  raise RuntimeError(
@@ -1301,7 +1320,6 @@ class AdaptiveDownloader:
1301
1320
  )
1302
1321
  raise RuntimeError(msg)
1303
1322
 
1304
-
1305
1323
  def _backoff(self, attempt: int, options: DownloadOptions) -> float:
1306
1324
  """Calculate retry delay based on strategy."""
1307
1325
  import random
@@ -1334,4 +1352,4 @@ class AdaptiveDownloader:
1334
1352
  @property
1335
1353
  def _result_total_size_mb(self) -> float:
1336
1354
  """Property used by result objects."""
1337
- return 0.0
1355
+ return 0.0
pygeofetch/core/engine.py CHANGED
@@ -484,4 +484,4 @@ class PyGeoFetch:
484
484
 
485
485
  def __repr__(self) -> str:
486
486
  authed = [item["provider"] for item in self.auth.list()]
487
- return f"PyGeoFetch(authenticated={authed}, cache_entries={len(self.searcher.cache._cache)})"
487
+ return f"PyGeoFetch(authenticated={authed}, cache_entries={len(self.searcher.cache._cache)})"
@@ -81,7 +81,8 @@ def generate_incremental_pairs(
81
81
  )
82
82
  neighbors = candidates_by_distance[:n_neighbors]
83
83
  for neighbor in neighbors:
84
- pair = tuple(sorted([new_date, neighbor]))
84
+ a, b = sorted([new_date, neighbor])
85
+ pair: Tuple[str, str] = (a, b)
85
86
  if pair not in seen:
86
87
  seen.add(pair)
87
88
  pairs.append(pair)
@@ -136,18 +137,28 @@ def plan_monitoring_run(
136
137
 
137
138
  if not new_dates:
138
139
  return MonitoringRunResult(
139
- new_dates=[], new_pairs=[], network_changed=False, ran_inversion=False,
140
+ new_dates=[],
141
+ new_pairs=[],
142
+ network_changed=False,
143
+ ran_inversion=False,
140
144
  message="No new dates since last run -- nothing to do.",
141
145
  )
142
146
 
143
- new_pairs = generate_incremental_pairs(new_dates, sorted(already_processed), n_neighbors)
147
+ new_pairs = generate_incremental_pairs(
148
+ new_dates, sorted(already_processed), n_neighbors
149
+ )
144
150
 
145
151
  logger.info(
146
152
  "Monitoring run: %d new date(s), %d new pair(s) needed (%d neighbors each).",
147
- len(new_dates), len(new_pairs), n_neighbors,
153
+ len(new_dates),
154
+ len(new_pairs),
155
+ n_neighbors,
148
156
  )
149
157
 
150
158
  return MonitoringRunResult(
151
- new_dates=new_dates, new_pairs=new_pairs, network_changed=True, ran_inversion=False,
159
+ new_dates=new_dates,
160
+ new_pairs=new_pairs,
161
+ network_changed=True,
162
+ ran_inversion=False,
152
163
  message=f"{len(new_dates)} new date(s), {len(new_pairs)} new pair(s) planned.",
153
164
  )
pygeofetch/core/state.py CHANGED
@@ -127,7 +127,9 @@ class ProjectState:
127
127
 
128
128
  # ── processed dates ─────────────────────────────────────────────
129
129
 
130
- def mark_dates_processed(self, dates: List[str], scene_ids: Optional[List[str]] = None):
130
+ def mark_dates_processed(
131
+ self, dates: List[str], scene_ids: Optional[List[Optional[str]]] = None
132
+ ):
131
133
  """Real, idempotent insert -- re-marking an already-processed date is a no-op, not an error."""
132
134
  now = datetime.now(timezone.utc).isoformat()
133
135
  scene_ids = scene_ids or [None] * len(dates)
@@ -139,12 +141,16 @@ class ProjectState:
139
141
 
140
142
  def processed_dates(self) -> List[str]:
141
143
  with self._connect() as conn:
142
- rows = conn.execute("SELECT date FROM processed_dates ORDER BY date").fetchall()
144
+ rows = conn.execute(
145
+ "SELECT date FROM processed_dates ORDER BY date"
146
+ ).fetchall()
143
147
  return [r[0] for r in rows]
144
148
 
145
149
  def is_date_processed(self, date: str) -> bool:
146
150
  with self._connect() as conn:
147
- row = conn.execute("SELECT 1 FROM processed_dates WHERE date = ?", (date,)).fetchone()
151
+ row = conn.execute(
152
+ "SELECT 1 FROM processed_dates WHERE date = ?", (date,)
153
+ ).fetchone()
148
154
  return row is not None
149
155
 
150
156
  # ── project metadata (last_download_timestamp, network hash, reference pixel) ──
@@ -159,7 +165,9 @@ class ProjectState:
159
165
 
160
166
  def _get_meta(self, key: str) -> Optional[str]:
161
167
  with self._connect() as conn:
162
- row = conn.execute("SELECT value FROM project_meta WHERE key = ?", (key,)).fetchone()
168
+ row = conn.execute(
169
+ "SELECT value FROM project_meta WHERE key = ?", (key,)
170
+ ).fetchone()
163
171
  return row[0] if row else None
164
172
 
165
173
  @property
@@ -226,7 +234,13 @@ class ProjectState:
226
234
  (limit,),
227
235
  ).fetchall()
228
236
  return [
229
- {"run_id": r[0], "started_at": r[1], "finished_at": r[2],
230
- "status": r[3], "n_new_scenes": r[4], "detail": r[5]}
237
+ {
238
+ "run_id": r[0],
239
+ "started_at": r[1],
240
+ "finished_at": r[2],
241
+ "status": r[3],
242
+ "n_new_scenes": r[4],
243
+ "detail": r[5],
244
+ }
231
245
  for r in rows
232
246
  ]
@@ -96,32 +96,6 @@ PS-InSAR densification and offset tracking::
96
96
  offsets = tracker.track(reference_amplitude, secondary_amplitude)
97
97
  """
98
98
 
99
- from pygeofetch.insar.atmosphere import AtmosphericCorrector
100
- from pygeofetch.insar.extraction import SLCExtractor
101
- from pygeofetch.insar.gpu import gpu_available
102
- from pygeofetch.insar.interferogram import InterferogramGenerator, InterferogramResult
103
- from pygeofetch.insar.timeseries import (
104
- SBASTimeSeries, PairCandidate, build_sbas_network,
105
- BurstSyncResult, generate_candidate_pairs,
106
- screen_stack_burst_synchronization, select_pairs_for_processing,
107
- select_reliable_reference_pixel, despike_velocity,
108
- InterferogramPair, TimeSeriesResult,
109
- )
110
- from pygeofetch.insar.unwrap import PhaseUnwrapper, multilook, goldstein_filter, bridge_unwrap_regions
111
- from pygeofetch.insar.synthetic import (
112
- okada_surface_deformation,
113
- displacement_to_los,
114
- spatially_correlated_field,
115
- generate_synthetic_interferogram,
116
- SyntheticInterferogramResult,
117
- )
118
- from pygeofetch.insar.validate import DataValidator, ValidationResult, PairClassification
119
- from pygeofetch.insar.visualize import (
120
- visualize_interferogram,
121
- visualize_timeseries,
122
- visualize_unwrapped,
123
- )
124
-
125
99
  # Real, orbit-based coregistration components. All exported here are
126
100
  # individually verified against known ground truth (see each module's
127
101
  # docstring for specifics): annotation.py against ESA's own field-path
@@ -132,39 +106,69 @@ from pygeofetch.insar.visualize import (
132
106
  # here — it has a known, unresolved reliability gap and is not
133
107
  # recommended as a primary coregistration path; use
134
108
  # compute_offset_field_from_dem() instead, which avoids it entirely.
135
- from pygeofetch.insar.annotation import SLCGeometry, parse_slc_geometry, BurstInfo, SwathTiming, parse_burst_info
136
- from pygeofetch.insar.deburst import compute_burst_row_ranges, deburst_array
137
- from pygeofetch.insar.flatearth import compute_flat_earth_phase
138
- from pygeofetch.insar.workflow import InSARProject
139
- from pygeofetch.insar.ionosphere import IonosphericCorrector, parse_ionex
140
- from pygeofetch.insar.stack_selection import (
141
- select_consistent_geometry, search_and_select_consistent_stack,
142
- select_burst_synchronized_dates, bbox_to_geojson_path, preview_search_results,
109
+ from pygeofetch.insar.annotation import (
110
+ BurstInfo,
111
+ SLCGeometry,
112
+ SwathTiming,
113
+ parse_burst_info,
114
+ parse_slc_geometry,
143
115
  )
144
- from pygeofetch.insar.preflight import PreflightGate, PreflightReport, PreflightIssue
116
+ from pygeofetch.insar.atmosphere import AtmosphericCorrector
117
+ from pygeofetch.insar.coregister import (
118
+ CoregistrationQuality,
119
+ collocate_by_geocoding,
120
+ compute_offset_field_from_dem,
121
+ fit_offset_polynomial,
122
+ fit_offset_polynomial_robust,
123
+ refine_offsets_by_coherence,
124
+ resample_with_offset_field,
125
+ )
126
+ from pygeofetch.insar.deburst import compute_burst_row_ranges, deburst_array
145
127
  from pygeofetch.insar.esd import (
128
+ SENTINEL1_IW_DELTA_F_OVL_HZ,
146
129
  compute_overlap_row_ranges,
147
130
  estimate_esd_shift_per_burst_overlap,
148
- SENTINEL1_IW_DELTA_F_OVL_HZ,
149
131
  )
132
+ from pygeofetch.insar.extraction import SLCExtractor
133
+ from pygeofetch.insar.flatearth import compute_flat_earth_phase
150
134
  from pygeofetch.insar.geolocation import (
151
- geodetic_to_ecef,
152
135
  find_zero_doppler_time,
153
- parse_orbit_file,
136
+ geodetic_to_ecef,
154
137
  interpolate_orbit_state,
155
138
  los_to_vertical_displacement,
139
+ parse_orbit_file,
156
140
  range_offset_to_vertical_displacement,
157
141
  )
158
- from pygeofetch.insar.coregister import (
159
- compute_offset_field_from_dem,
160
- fit_offset_polynomial,
161
- fit_offset_polynomial_robust,
162
- refine_offsets_by_coherence,
163
- resample_with_offset_field,
164
- collocate_by_geocoding,
165
- CoregistrationQuality,
142
+ from pygeofetch.insar.gpu import gpu_available
143
+ from pygeofetch.insar.interferogram import InterferogramGenerator, InterferogramResult
144
+ from pygeofetch.insar.ionosphere import IonosphericCorrector, parse_ionex
145
+
146
+ # Range/azimuth pixel offsets to ground East/North/Up displacement. The
147
+ # math is verified (solve_enu_displacement agrees with
148
+ # range_offset_to_vertical_displacement to 9 decimal places on the same
149
+ # known scenario, in both operating modes). heading_angle_deg is NOT
150
+ # verified against any real product's orbit metadata — every test this
151
+ # session used a typical Sentinel-1 descending value (190 deg). Pull the
152
+ # real value from real product metadata before using this in production;
153
+ # see this module's own docstring for the full caveat.
154
+ from pygeofetch.insar.offset_geometry import (
155
+ pixel_to_physical_offsets,
156
+ solve_enu_displacement,
166
157
  )
167
158
 
159
+ # Amplitude-based offset tracking. NCC verified to machine precision
160
+ # against the direct textbook definition; sub-pixel refinement verified
161
+ # to <0.06px on a blind synthetic shift; the full OffsetTracker windowing
162
+ # class verified on a spatially-VARYING synthetic field, not just a
163
+ # uniform shift.
164
+ from pygeofetch.insar.offset_tracking import (
165
+ OffsetTracker,
166
+ OffsetTrackingResult,
167
+ compute_snr,
168
+ normalized_cross_correlation,
169
+ subpixel_peak_offset,
170
+ )
171
+ from pygeofetch.insar.preflight import PreflightGate, PreflightIssue, PreflightReport
168
172
  from pygeofetch.insar.provenance import write_provenance_manifest
169
173
 
170
174
  # Persistent Scatterer InSAR (Ferretti, Prati & Rocca 2001). Real, tested
@@ -176,34 +180,55 @@ from pygeofetch.insar.provenance import write_provenance_manifest
176
180
  from pygeofetch.insar.ps_selection import (
177
181
  PSSelectionResult,
178
182
  compute_amplitude_dispersion_index,
183
+ estimate_atmospheric_phase_screen,
184
+ refine_ps_mask_with_temporal_coherence,
179
185
  select_persistent_scatterers,
180
186
  temporal_coherence,
181
- refine_ps_mask_with_temporal_coherence,
182
- estimate_atmospheric_phase_screen,
183
187
  )
184
-
185
- # Amplitude-based offset tracking. NCC verified to machine precision
186
- # against the direct textbook definition; sub-pixel refinement verified
187
- # to <0.06px on a blind synthetic shift; the full OffsetTracker windowing
188
- # class verified on a spatially-VARYING synthetic field, not just a
189
- # uniform shift.
190
- from pygeofetch.insar.offset_tracking import (
191
- OffsetTrackingResult,
192
- OffsetTracker,
193
- normalized_cross_correlation,
194
- subpixel_peak_offset,
195
- compute_snr,
188
+ from pygeofetch.insar.stack_selection import (
189
+ bbox_to_geojson_path,
190
+ preview_search_results,
191
+ search_and_select_consistent_stack,
192
+ select_burst_synchronized_dates,
193
+ select_consistent_geometry,
196
194
  )
197
-
198
- # Range/azimuth pixel offsets to ground East/North/Up displacement. The
199
- # math is verified (solve_enu_displacement agrees with
200
- # range_offset_to_vertical_displacement to 9 decimal places on the same
201
- # known scenario, in both operating modes). heading_angle_deg is NOT
202
- # verified against any real product's orbit metadata — every test this
203
- # session used a typical Sentinel-1 descending value (190 deg). Pull the
204
- # real value from real product metadata before using this in production;
205
- # see this module's own docstring for the full caveat.
206
- from pygeofetch.insar.offset_geometry import pixel_to_physical_offsets, solve_enu_displacement
195
+ from pygeofetch.insar.synthetic import (
196
+ SyntheticInterferogramResult,
197
+ displacement_to_los,
198
+ generate_synthetic_interferogram,
199
+ okada_surface_deformation,
200
+ spatially_correlated_field,
201
+ )
202
+ from pygeofetch.insar.timeseries import (
203
+ BurstSyncResult,
204
+ InterferogramPair,
205
+ PairCandidate,
206
+ SBASTimeSeries,
207
+ TimeSeriesResult,
208
+ build_sbas_network,
209
+ despike_velocity,
210
+ generate_candidate_pairs,
211
+ screen_stack_burst_synchronization,
212
+ select_pairs_for_processing,
213
+ select_reliable_reference_pixel,
214
+ )
215
+ from pygeofetch.insar.unwrap import (
216
+ PhaseUnwrapper,
217
+ bridge_unwrap_regions,
218
+ goldstein_filter,
219
+ multilook,
220
+ )
221
+ from pygeofetch.insar.validate import (
222
+ DataValidator,
223
+ PairClassification,
224
+ ValidationResult,
225
+ )
226
+ from pygeofetch.insar.visualize import (
227
+ visualize_interferogram,
228
+ visualize_timeseries,
229
+ visualize_unwrapped,
230
+ )
231
+ from pygeofetch.insar.workflow import InSARProject
207
232
 
208
233
  __all__ = [
209
234
  "InterferogramGenerator",
@@ -286,4 +311,4 @@ __all__ = [
286
311
  "compute_snr",
287
312
  "pixel_to_physical_offsets",
288
313
  "solve_enu_displacement",
289
- ]
314
+ ]