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.
- pygeofetch/__init__.py +1 -1
- pygeofetch/cli/auth_commands.py +6 -3
- pygeofetch/cli/main.py +10 -6
- pygeofetch/cli/monitor_commands.py +49 -14
- pygeofetch/cli/search_commands.py +1 -1
- pygeofetch/core/authenticator.py +5 -3
- pygeofetch/core/downloader.py +31 -13
- pygeofetch/core/engine.py +1 -1
- pygeofetch/core/monitor.py +16 -5
- pygeofetch/core/state.py +20 -6
- pygeofetch/insar/__init__.py +96 -71
- pygeofetch/insar/analysis.py +62 -40
- pygeofetch/insar/annotation.py +116 -70
- pygeofetch/insar/atmosphere.py +168 -70
- pygeofetch/insar/coregister.py +126 -58
- pygeofetch/insar/deburst.py +39 -12
- pygeofetch/insar/esd.py +65 -28
- pygeofetch/insar/extraction.py +126 -67
- pygeofetch/insar/flatearth.py +45 -18
- pygeofetch/insar/geolocation.py +79 -29
- pygeofetch/insar/gpu.py +3 -1
- pygeofetch/insar/interferogram.py +502 -193
- pygeofetch/insar/ionosphere.py +118 -45
- pygeofetch/insar/offset_geometry.py +47 -43
- pygeofetch/insar/offset_tracking.py +45 -20
- pygeofetch/insar/preflight.py +288 -169
- pygeofetch/insar/provenance.py +2 -1
- pygeofetch/insar/ps_selection.py +21 -9
- pygeofetch/insar/splitspectrum_diagnostic.py +12 -4
- pygeofetch/insar/stack_selection.py +179 -77
- pygeofetch/insar/synthetic.py +53 -17
- pygeofetch/insar/timeseries.py +268 -152
- pygeofetch/insar/unwrap.py +408 -156
- pygeofetch/insar/validate.py +36 -13
- pygeofetch/insar/visualize.py +15 -5
- pygeofetch/insar/workflow.py +102 -29
- pygeofetch/meta.yaml +1 -1
- pygeofetch/models/satellite_data.py +5 -3
- pygeofetch/processing/indices.py +5 -0
- pygeofetch/processing/preprocessor.py +55 -25
- pygeofetch/processing/sar.py +13 -4
- pygeofetch/processor/indices.py +4 -0
- pygeofetch/processor/stacker.py +4 -0
- pygeofetch/processor/timeseries.py +41 -13
- pygeofetch/providers/airbus_oneatlas.py +17 -4
- pygeofetch/providers/alaska_satellite_facility.py +5 -2
- pygeofetch/providers/aws_earth.py +1 -1
- pygeofetch/providers/copernicus.py +19 -8
- pygeofetch/providers/copernicus_nodes.py +73 -23
- pygeofetch/providers/digitalglobe.py +5 -2
- pygeofetch/providers/earth_explorer_additional.py +5 -2
- pygeofetch/providers/element84.py +1 -1
- pygeofetch/providers/eodag_provider.py +9 -5
- pygeofetch/providers/esa_scihub.py +17 -4
- pygeofetch/providers/geoserver_generic.py +5 -2
- pygeofetch/providers/google_earth_engine.py +17 -4
- pygeofetch/providers/inpe_cbers.py +5 -2
- pygeofetch/providers/isro_bhuvan.py +5 -2
- pygeofetch/providers/jaxa_earth.py +5 -2
- pygeofetch/providers/maxar_gbdx.py +5 -2
- pygeofetch/providers/nasa_earthdata.py +6 -4
- pygeofetch/providers/nasa_earthdata_cloud.py +2 -2
- pygeofetch/providers/noaa_big_data.py +5 -2
- pygeofetch/providers/opentopography.py +6 -2
- pygeofetch/providers/planet.py +4 -3
- pygeofetch/providers/planetary_computer.py +1 -1
- pygeofetch/providers/sentinel_hub.py +11 -7
- pygeofetch/providers/terrabotics.py +4 -3
- pygeofetch/providers/usgs.py +20 -5
- pygeofetch/sar/__init__.py +1 -1
- pygeofetch/sar/_native.py +1 -1
- pygeofetch/sar/extraction.py +8 -4
- pygeofetch/utils/gcp_recovery.py +1 -3
- pygeofetch/viz/map.py +82 -33
- pygeofetch/viz/plot.py +91 -38
- {pygeofetch-2.6.2.dist-info → pygeofetch-2.6.2.1.dist-info}/METADATA +25 -28
- pygeofetch-2.6.2.1.dist-info/RECORD +122 -0
- pygeofetch-2.6.2.dist-info/RECORD +0 -122
- {pygeofetch-2.6.2.dist-info → pygeofetch-2.6.2.1.dist-info}/WHEEL +0 -0
- {pygeofetch-2.6.2.dist-info → pygeofetch-2.6.2.1.dist-info}/entry_points.txt +0 -0
- {pygeofetch-2.6.2.dist-info → pygeofetch-2.6.2.1.dist-info}/licenses/LICENSE +0 -0
- {pygeofetch-2.6.2.dist-info → pygeofetch-2.6.2.1.dist-info}/top_level.txt +0 -0
pygeofetch/__init__.py
CHANGED
pygeofetch/cli/auth_commands.py
CHANGED
|
@@ -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.
|
|
225
|
-
|
|
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(
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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(
|
|
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(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@click.option(
|
|
46
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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=
|
|
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(
|
pygeofetch/core/authenticator.py
CHANGED
|
@@ -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":
|
|
451
|
-
|
|
452
|
-
|
|
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
|
pygeofetch/core/downloader.py
CHANGED
|
@@ -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(
|
|
369
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
918
|
-
|
|
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,
|
|
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)})"
|
pygeofetch/core/monitor.py
CHANGED
|
@@ -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
|
-
|
|
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=[],
|
|
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(
|
|
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),
|
|
153
|
+
len(new_dates),
|
|
154
|
+
len(new_pairs),
|
|
155
|
+
n_neighbors,
|
|
148
156
|
)
|
|
149
157
|
|
|
150
158
|
return MonitoringRunResult(
|
|
151
|
-
new_dates=new_dates,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
{
|
|
230
|
-
|
|
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
|
]
|
pygeofetch/insar/__init__.py
CHANGED
|
@@ -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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
]
|