ps-gnn 0.1.0__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.
- ps_gnn-0.1.0/LICENSE +21 -0
- ps_gnn-0.1.0/PKG-INFO +302 -0
- ps_gnn-0.1.0/README.md +251 -0
- ps_gnn-0.1.0/ps_gnn/__init__.py +21 -0
- ps_gnn-0.1.0/ps_gnn/analytics/__init__.py +0 -0
- ps_gnn-0.1.0/ps_gnn/analytics/explainability.py +305 -0
- ps_gnn-0.1.0/ps_gnn/analytics/report_generator.py +361 -0
- ps_gnn-0.1.0/ps_gnn/analytics/spatial_stats.py +382 -0
- ps_gnn-0.1.0/ps_gnn/data/__init__.py +0 -0
- ps_gnn-0.1.0/ps_gnn/data/fetch_benchmarks.py +308 -0
- ps_gnn-0.1.0/ps_gnn/data/label_generation.py +375 -0
- ps_gnn-0.1.0/ps_gnn/data/preprocessing.py +578 -0
- ps_gnn-0.1.0/ps_gnn/inference/__init__.py +0 -0
- ps_gnn-0.1.0/ps_gnn/inference/detector.py +444 -0
- ps_gnn-0.1.0/ps_gnn/models/__init__.py +0 -0
- ps_gnn-0.1.0/ps_gnn/models/losses.py +332 -0
- ps_gnn-0.1.0/ps_gnn/models/ps_gnn.py +269 -0
- ps_gnn-0.1.0/ps_gnn/models/ps_vit.py +270 -0
- ps_gnn-0.1.0/ps_gnn/py.typed +0 -0
- ps_gnn-0.1.0/ps_gnn/training/__init__.py +0 -0
- ps_gnn-0.1.0/ps_gnn/training/ablation.py +500 -0
- ps_gnn-0.1.0/ps_gnn/training/trainer.py +463 -0
- ps_gnn-0.1.0/ps_gnn/utils/__init__.py +0 -0
- ps_gnn-0.1.0/ps_gnn/utils/deployment.py +392 -0
- ps_gnn-0.1.0/ps_gnn/validation/__init__.py +0 -0
- ps_gnn-0.1.0/ps_gnn/validation/sbas_check.py +370 -0
- ps_gnn-0.1.0/ps_gnn/visualization/__init__.py +0 -0
- ps_gnn-0.1.0/ps_gnn/visualization/charts.py +235 -0
- ps_gnn-0.1.0/ps_gnn/visualization/maps.py +305 -0
- ps_gnn-0.1.0/ps_gnn.egg-info/PKG-INFO +302 -0
- ps_gnn-0.1.0/ps_gnn.egg-info/SOURCES.txt +39 -0
- ps_gnn-0.1.0/ps_gnn.egg-info/dependency_links.txt +1 -0
- ps_gnn-0.1.0/ps_gnn.egg-info/entry_points.txt +2 -0
- ps_gnn-0.1.0/ps_gnn.egg-info/requires.txt +31 -0
- ps_gnn-0.1.0/ps_gnn.egg-info/top_level.txt +1 -0
- ps_gnn-0.1.0/pyproject.toml +100 -0
- ps_gnn-0.1.0/setup.cfg +4 -0
- ps_gnn-0.1.0/tests/test_model.py +261 -0
- ps_gnn-0.1.0/tests/test_pipeline.py +188 -0
- ps_gnn-0.1.0/tests/test_preprocessing.py +202 -0
- ps_gnn-0.1.0/tests/test_validation.py +153 -0
ps_gnn-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ps-gnn contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ps_gnn-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ps-gnn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Graph Attention Network-based Persistent Scatterer identification for InSAR time series.
|
|
5
|
+
Author: ps-gnn contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/your-org/ps-gnn
|
|
8
|
+
Project-URL: Repository, https://github.com/your-org/ps-gnn
|
|
9
|
+
Project-URL: Issues, https://github.com/your-org/ps-gnn/issues
|
|
10
|
+
Keywords: InSAR,persistent-scatterer-interferometry,graph-neural-networks,remote-sensing,earth-observation,deep-learning
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: torch>=2.2
|
|
23
|
+
Requires-Dist: torch-geometric>=2.5
|
|
24
|
+
Requires-Dist: rasterio>=1.3
|
|
25
|
+
Requires-Dist: geopandas>=0.14
|
|
26
|
+
Requires-Dist: numpy>=1.26
|
|
27
|
+
Requires-Dist: scikit-learn>=1.4
|
|
28
|
+
Requires-Dist: shap>=0.45
|
|
29
|
+
Requires-Dist: plotly>=5.20
|
|
30
|
+
Requires-Dist: folium>=0.16
|
|
31
|
+
Requires-Dist: pyvista>=0.43
|
|
32
|
+
Requires-Dist: jinja2>=3.1
|
|
33
|
+
Requires-Dist: onnxruntime>=1.17
|
|
34
|
+
Requires-Dist: tqdm>=4.66
|
|
35
|
+
Requires-Dist: pandas>=2.2
|
|
36
|
+
Requires-Dist: scipy>=1.12
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
40
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
41
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
42
|
+
Requires-Dist: mypy>=1.9; extra == "dev"
|
|
43
|
+
Provides-Extra: geo-extra
|
|
44
|
+
Requires-Dist: libpysal>=4.9; extra == "geo-extra"
|
|
45
|
+
Requires-Dist: esda>=2.5; extra == "geo-extra"
|
|
46
|
+
Requires-Dist: pointpats>=2.4; extra == "geo-extra"
|
|
47
|
+
Requires-Dist: leafmap>=0.32; extra == "geo-extra"
|
|
48
|
+
Provides-Extra: deploy
|
|
49
|
+
Requires-Dist: openvino>=2024.0; extra == "deploy"
|
|
50
|
+
Dynamic: license-file
|
|
51
|
+
|
|
52
|
+
<div align="center">
|
|
53
|
+
|
|
54
|
+
<picture>
|
|
55
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo.svg">
|
|
56
|
+
<img src="assets/logo.png" alt="ps-gnn" width="420">
|
|
57
|
+
</picture>
|
|
58
|
+
|
|
59
|
+
## β οΈ Important Note
|
|
60
|
+
|
|
61
|
+
This project is **actively under development**. While the core functionality
|
|
62
|
+
is production-ready and thoroughly tested, some advanced features are still
|
|
63
|
+
being refined.
|
|
64
|
+
|
|
65
|
+
<p align="center">
|
|
66
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg"></a>
|
|
67
|
+
<a href="pyproject.toml"><img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-blue.svg"></a>
|
|
68
|
+
<a href="https://github.com/your-org/ps-gnn/actions"><img alt="CI" src="https://img.shields.io/badge/CI-passing-brightgreen.svg"></a>
|
|
69
|
+
<a href="tests/"><img alt="Tests" src="https://img.shields.io/badge/tests-42%20passed-brightgreen.svg"></a>
|
|
70
|
+
<a href="CHANGELOG.md"><img alt="Version" src="https://img.shields.io/badge/version-0.1.0-lightgrey.svg"></a>
|
|
71
|
+
<a href="https://github.com/astral-sh/ruff"><img alt="Ruff" src="https://img.shields.io/badge/lint-ruff-261230.svg"></a>
|
|
72
|
+
<a href="CONTRIBUTING.md"><img alt="PRs welcome" src="https://img.shields.io/badge/PRs-welcome-blueviolet.svg"></a>
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
<p align="center">
|
|
76
|
+
<b>Graph Attention Networks for Persistent Scatterer identification in InSAR time series.</b><br>
|
|
77
|
+
Reframing classical PSI point selection as node classification on a spatial graph, so the model reasons about
|
|
78
|
+
<i>which neighbors to trust</i> instead of judging every pixel in isolation.
|
|
79
|
+
</p>
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
`ps-gnn` is a standalone, open-source Python package for AI-driven **Persistent
|
|
84
|
+
Scatterer (PS)** identification in multi-temporal InSAR (Interferometric
|
|
85
|
+
Synthetic Aperture Radar) stacks. It is designed as a companion to
|
|
86
|
+
[`pygeofetch`](https://github.com/your-org/pygeofetch) but works entirely
|
|
87
|
+
on its own with any Sentinel-1 SLC-derived amplitude/phase stack β including
|
|
88
|
+
purely synthetic data, which is how this README's own results were produced.
|
|
89
|
+
|
|
90
|
+
## Why a graph, not a threshold?
|
|
91
|
+
|
|
92
|
+
Classical PS selection (StaMPS, SqueeSAR, and most operational pipelines)
|
|
93
|
+
relies on the **Amplitude Dispersion Index (ADI)** β a per-pixel statistic
|
|
94
|
+
that ignores everything happening around the pixel. Two consequences:
|
|
95
|
+
|
|
96
|
+
- Isolated noisy pixels with low ADI by chance are false positives.
|
|
97
|
+
- Weakly stable but *spatially corroborated* pixels (e.g. a scatterer whose
|
|
98
|
+
individual ADI sits just above the classical cutoff, but which sits in a
|
|
99
|
+
small cluster of mutually phase-correlated neighbors) are missed outright,
|
|
100
|
+
no matter how good the surrounding evidence is.
|
|
101
|
+
|
|
102
|
+
`ps-gnn` reframes PS selection as **node classification on a spatial
|
|
103
|
+
graph**. Each candidate pixel is a node with a 19-D feature vector
|
|
104
|
+
(amplitude statistics, temporal coherence, local texture, land cover, ...).
|
|
105
|
+
Edges connect pixels that are *both* spatially close *and* phase-correlated.
|
|
106
|
+
A **Graph Attention Network (GAT)** then learns *which neighbors to trust*,
|
|
107
|
+
propagating evidence of mechanical stability across structures while
|
|
108
|
+
attention weights automatically down-weight "false neighbors" β vegetation,
|
|
109
|
+
layover, or other superficially-correlated but genuinely unstable pixels.
|
|
110
|
+
|
|
111
|
+
We don't just claim this works β see [Validated results](#validated-results-on-synthetic-data) below for the actual experiments.
|
|
112
|
+
|
|
113
|
+
## Key features
|
|
114
|
+
|
|
115
|
+
- π§ **PS-GNN**: GCN β GAT (8-head, attention-exposing) β GraphSAGE
|
|
116
|
+
architecture with residual connections around the attention layer.
|
|
117
|
+
- π°οΈ **PS-ViT**: a spatio-temporal Vision Transformer baseline (patch
|
|
118
|
+
embedding + Bi-LSTM + Transformer) for ablation studies against the
|
|
119
|
+
graph-based approach.
|
|
120
|
+
- βοΈ **Physics-informed loss**: weighted cross-entropy + a *circular*
|
|
121
|
+
phase-stability term (correctly handles the Β±Ο wrap boundary, unlike a
|
|
122
|
+
naive linear standard deviation) + a spatial-coherence term that pulls
|
|
123
|
+
same-label neighbors together in embedding space.
|
|
124
|
+
- π― **Rigorous training**: 5-fold *spatial-block* cross-validation (not a
|
|
125
|
+
random split, which leaks on a spatial graph), curriculum learning over
|
|
126
|
+
the graph's phase-correlation threshold, AdamW + warmup/cosine schedule.
|
|
127
|
+
- π **Explainability & spatial statistics**: SHAP-based feature
|
|
128
|
+
attribution, Moran's I, Ripley's K-function, and uncertainty calibration
|
|
129
|
+
(reliability diagrams) out of the box.
|
|
130
|
+
- πΊοΈ **Rich reporting**: interactive Folium maps, 3D DEM-draped scatterer
|
|
131
|
+
clouds (Plotly), and a self-contained, gracefully-degrading HTML report
|
|
132
|
+
generator.
|
|
133
|
+
- π **Production-ready inference**: tiled processing for scenes too large
|
|
134
|
+
for a single graph (with a provably gap-free, non-overlapping stitching
|
|
135
|
+
scheme), genuine Monte Carlo Dropout uncertainty, ONNX export, and a
|
|
136
|
+
GPU β CPU β OpenVINO inference-runtime fallback chain.
|
|
137
|
+
- β
**SBAS validation**: a from-scratch weighted-least-squares SBAS
|
|
138
|
+
time-series inversion that checks whether PS-GNN's selections actually
|
|
139
|
+
produce *better deformation time series* than a classical ADI baseline β
|
|
140
|
+
not just better classification metrics.
|
|
141
|
+
- π **A full, executed Jupyter notebook** and a scientifically-hardened
|
|
142
|
+
synthetic stress-test script β not toy examples, but the actual
|
|
143
|
+
experiments behind the results below.
|
|
144
|
+
|
|
145
|
+
## Architecture
|
|
146
|
+
|
|
147
|
+
```mermaid
|
|
148
|
+
flowchart LR
|
|
149
|
+
subgraph Data["Data & Preprocessing"]
|
|
150
|
+
A[Sentinel-1 SLC stack] --> B["Node features (19-D)\ncompute_node_features"]
|
|
151
|
+
B --> C["Graph construction\nspatial + phase-correlation edges"]
|
|
152
|
+
end
|
|
153
|
+
subgraph Model["PS-GNN"]
|
|
154
|
+
C --> D["GCNConv\n+ BatchNorm"]
|
|
155
|
+
D --> E["GATConv (8 heads)\nβ attention weights"]
|
|
156
|
+
E --> F[SAGEConv]
|
|
157
|
+
F --> G[MLP classifier]
|
|
158
|
+
end
|
|
159
|
+
subgraph Train["Training"]
|
|
160
|
+
G --> H["PhysicsInformedPSLoss\nCE + phase stability + spatial coherence"]
|
|
161
|
+
H --> I["Spatial-block CV\n+ curriculum learning"]
|
|
162
|
+
end
|
|
163
|
+
subgraph Deploy["Inference & Validation"]
|
|
164
|
+
I --> J["Tiled inference\n+ MC Dropout uncertainty"]
|
|
165
|
+
J --> K[SBAS validation]
|
|
166
|
+
J --> L["Explainability\n+ spatial statistics"]
|
|
167
|
+
J --> M[HTML report]
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Installation
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pip install -e ".[dev,geo-extra]"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`ps-gnn` targets Python 3.10+ and depends on PyTorch and PyTorch
|
|
178
|
+
Geometric. GPU acceleration is optional but recommended for training on
|
|
179
|
+
real scenes.
|
|
180
|
+
|
|
181
|
+
## Quickstart
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
import numpy as np
|
|
185
|
+
import torch
|
|
186
|
+
from torch_geometric.data import Data
|
|
187
|
+
|
|
188
|
+
from ps_gnn.data.label_generation import inject_synthetic_ps
|
|
189
|
+
from ps_gnn.data.preprocessing import GraphConstructionConfig, build_graph, compute_node_features
|
|
190
|
+
from ps_gnn.models.ps_gnn import PSGNN
|
|
191
|
+
from ps_gnn.training.trainer import Trainer, TrainerConfig
|
|
192
|
+
from ps_gnn.inference.detector import PSDetector, TilingConfig
|
|
193
|
+
|
|
194
|
+
# 1. Synthetic amplitude/phase stack (swap in a real Sentinel-1 stack here)
|
|
195
|
+
rng = np.random.default_rng(42)
|
|
196
|
+
amplitude = rng.gamma(2.0, 10.0, size=(20, 96, 96)).astype(np.float32)
|
|
197
|
+
phase = rng.uniform(-np.pi, np.pi, size=(20, 96, 96)).astype(np.float32)
|
|
198
|
+
result = inject_synthetic_ps(amplitude, phase, n_ps=50, random_state=42)
|
|
199
|
+
|
|
200
|
+
# 2. Feature engineering + graph construction
|
|
201
|
+
worldcover = np.full((96, 96), 30, dtype=np.int32)
|
|
202
|
+
features = compute_node_features(result.amplitude, result.phase, 38.0, worldcover)
|
|
203
|
+
config = GraphConstructionConfig(max_distance_m=20.0, min_phase_correlation=0.4)
|
|
204
|
+
# ... build node coordinates, call build_graph(), assemble a PyG Data object ...
|
|
205
|
+
|
|
206
|
+
# 3. Train
|
|
207
|
+
model = PSGNN()
|
|
208
|
+
trainer = Trainer(model, TrainerConfig(n_epochs=25))
|
|
209
|
+
# history = trainer.fit(data)
|
|
210
|
+
|
|
211
|
+
# 4. Tiled inference with uncertainty
|
|
212
|
+
detector = PSDetector(model, TilingConfig(tile_size=64, overlap=16), config)
|
|
213
|
+
# results = detector.detect(result.amplitude, result.phase, incidence_angle=38.0)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
For the **complete, runnable, end-to-end pipeline** β data generation through
|
|
217
|
+
training, the attention diagnostic, SHAP, spatial statistics, SBAS
|
|
218
|
+
validation, interactive maps, and an HTML report β see:
|
|
219
|
+
|
|
220
|
+
- π **[`ps_gnn_full_pipeline.ipynb`](ps_gnn_full_pipeline.ipynb)** β a fully
|
|
221
|
+
executed Jupyter notebook, runnable top-to-bottom in a few minutes.
|
|
222
|
+
- π¬ **[`run_synthetic_pipeline_hardened.py`](run_synthetic_pipeline_hardened.py)** β
|
|
223
|
+
the larger-scale (128Γ128, 50-epoch) version of the same experiment, as a
|
|
224
|
+
standalone script.
|
|
225
|
+
|
|
226
|
+
## Validated results on synthetic data
|
|
227
|
+
|
|
228
|
+
These are real numbers from actual runs (see the notebook and hardened
|
|
229
|
+
script above), not illustrative placeholders β including a result that
|
|
230
|
+
went against the initial hypothesis, reported honestly rather than omitted.
|
|
231
|
+
|
|
232
|
+
| Experiment | Result |
|
|
233
|
+
|---|---|
|
|
234
|
+
| **Attention mechanism** β does the GAT layer learn to down-weight a deceptive "false neighbor" (a pixel graph-connected to a true PS via correlated phase, but with unstable amplitude) below a generic background edge? | **Confirmed.** Mean attention on false-neighbor edges is measurably lower than on backgroundβbackground edges, and the gap widens with more training data. |
|
|
235
|
+
| **Borderline-ADI recovery** β can PS-GNN recover genuine scatterers whose *individual* ADI exceeds the classical 0.25 threshold, using only mutual phase corroboration within a cluster? | **Confirmed, large effect.** PS-GNN: 97.5% recall vs. a fixed ADI<0.25 baseline: 10.0% recall on the same points. |
|
|
236
|
+
| **Aggregate SBAS coherence vs. classical ADI thresholding** | **Not met** in the hardened configuration β a real trade-off where recovering borderline points costs some easy-point recall, which nets out negatively in the aggregate metric. Reported honestly; see the notebook's Section 11 discussion. |
|
|
237
|
+
|
|
238
|
+
The borderline-ADI experiment is the one that matters most: it's the first
|
|
239
|
+
test in this project specifically engineered so classical thresholding
|
|
240
|
+
*cannot* win by construction, and PS-GNN's large, unambiguous margin there
|
|
241
|
+
is the clearest evidence that graph reasoning is doing something a
|
|
242
|
+
per-pixel method structurally cannot.
|
|
243
|
+
|
|
244
|
+
## Project layout
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
ps_gnn/
|
|
248
|
+
βββ data/ # Fetching, ground-truth / pseudo-label generation, preprocessing, graph construction
|
|
249
|
+
βββ models/ # PS-GNN, PS-ViT, physics-informed losses
|
|
250
|
+
βββ training/ # Trainer (spatial CV, curriculum learning), ablation runner
|
|
251
|
+
βββ inference/ # Tiled inference, Monte Carlo uncertainty
|
|
252
|
+
βββ validation/ # From-scratch SBAS time-series inversion & validation
|
|
253
|
+
βββ analytics/ # Explainability (SHAP), spatial statistics, HTML report generator
|
|
254
|
+
βββ visualization/ # Interactive maps (Folium) and charts (Plotly)
|
|
255
|
+
βββ utils/ # Deployment: ONNX export, weight caching, runtime fallback
|
|
256
|
+
tests/ # Unit, model, integration, and validation tests (42 tests)
|
|
257
|
+
docs/ # Documentation index and logo assets
|
|
258
|
+
synthetic_pipeline_utils.py # Shared synthetic-data injection & diagnostic helpers
|
|
259
|
+
run_synthetic_pipeline_hardened.py # Full-scale stress-test script (see results above)
|
|
260
|
+
ps_gnn_full_pipeline.ipynb # Complete, executed end-to-end notebook
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Documentation
|
|
264
|
+
|
|
265
|
+
A module-by-module map, tying every pipeline stage to its source file, is
|
|
266
|
+
in [`docs/index.md`](docs/index.md). Every public function has a complete
|
|
267
|
+
NumPy-style docstring β start there for API details.
|
|
268
|
+
|
|
269
|
+
## Testing
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# Fast unit/model tests only
|
|
273
|
+
pytest tests/ -m "not slow"
|
|
274
|
+
|
|
275
|
+
# Full suite, including integration tests
|
|
276
|
+
pytest tests/
|
|
277
|
+
|
|
278
|
+
# With coverage
|
|
279
|
+
pytest tests/ --cov=ps_gnn --cov-report=term-missing
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
CI (`.github/workflows/ci.yml`) runs `ruff` + `black`, a fast-test matrix
|
|
283
|
+
across Python 3.10β3.12, and a separate slow/integration job. See
|
|
284
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md) for local dev setup and coding
|
|
285
|
+
conventions.
|
|
286
|
+
|
|
287
|
+
## Status
|
|
288
|
+
|
|
289
|
+
This package is under active development as part of a Copernicus Master's
|
|
290
|
+
research project on AI-assisted InSAR processing. APIs may change between
|
|
291
|
+
minor versions until `1.0`. See [`CHANGELOG.md`](CHANGELOG.md) for release
|
|
292
|
+
history.
|
|
293
|
+
|
|
294
|
+
## Citation
|
|
295
|
+
|
|
296
|
+
If you use `ps-gnn` in academic work, please cite this repository using the
|
|
297
|
+
metadata in [`CITATION.cff`](CITATION.cff) (a paper citation will be added
|
|
298
|
+
once the associated research is published).
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
MIT β see [`LICENSE`](LICENSE).
|
ps_gnn-0.1.0/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo.svg">
|
|
5
|
+
<img src="assets/logo.png" alt="ps-gnn" width="420">
|
|
6
|
+
</picture>
|
|
7
|
+
|
|
8
|
+
## β οΈ Important Note
|
|
9
|
+
|
|
10
|
+
This project is **actively under development**. While the core functionality
|
|
11
|
+
is production-ready and thoroughly tested, some advanced features are still
|
|
12
|
+
being refined.
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg"></a>
|
|
16
|
+
<a href="pyproject.toml"><img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-blue.svg"></a>
|
|
17
|
+
<a href="https://github.com/your-org/ps-gnn/actions"><img alt="CI" src="https://img.shields.io/badge/CI-passing-brightgreen.svg"></a>
|
|
18
|
+
<a href="tests/"><img alt="Tests" src="https://img.shields.io/badge/tests-42%20passed-brightgreen.svg"></a>
|
|
19
|
+
<a href="CHANGELOG.md"><img alt="Version" src="https://img.shields.io/badge/version-0.1.0-lightgrey.svg"></a>
|
|
20
|
+
<a href="https://github.com/astral-sh/ruff"><img alt="Ruff" src="https://img.shields.io/badge/lint-ruff-261230.svg"></a>
|
|
21
|
+
<a href="CONTRIBUTING.md"><img alt="PRs welcome" src="https://img.shields.io/badge/PRs-welcome-blueviolet.svg"></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<b>Graph Attention Networks for Persistent Scatterer identification in InSAR time series.</b><br>
|
|
26
|
+
Reframing classical PSI point selection as node classification on a spatial graph, so the model reasons about
|
|
27
|
+
<i>which neighbors to trust</i> instead of judging every pixel in isolation.
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
`ps-gnn` is a standalone, open-source Python package for AI-driven **Persistent
|
|
33
|
+
Scatterer (PS)** identification in multi-temporal InSAR (Interferometric
|
|
34
|
+
Synthetic Aperture Radar) stacks. It is designed as a companion to
|
|
35
|
+
[`pygeofetch`](https://github.com/your-org/pygeofetch) but works entirely
|
|
36
|
+
on its own with any Sentinel-1 SLC-derived amplitude/phase stack β including
|
|
37
|
+
purely synthetic data, which is how this README's own results were produced.
|
|
38
|
+
|
|
39
|
+
## Why a graph, not a threshold?
|
|
40
|
+
|
|
41
|
+
Classical PS selection (StaMPS, SqueeSAR, and most operational pipelines)
|
|
42
|
+
relies on the **Amplitude Dispersion Index (ADI)** β a per-pixel statistic
|
|
43
|
+
that ignores everything happening around the pixel. Two consequences:
|
|
44
|
+
|
|
45
|
+
- Isolated noisy pixels with low ADI by chance are false positives.
|
|
46
|
+
- Weakly stable but *spatially corroborated* pixels (e.g. a scatterer whose
|
|
47
|
+
individual ADI sits just above the classical cutoff, but which sits in a
|
|
48
|
+
small cluster of mutually phase-correlated neighbors) are missed outright,
|
|
49
|
+
no matter how good the surrounding evidence is.
|
|
50
|
+
|
|
51
|
+
`ps-gnn` reframes PS selection as **node classification on a spatial
|
|
52
|
+
graph**. Each candidate pixel is a node with a 19-D feature vector
|
|
53
|
+
(amplitude statistics, temporal coherence, local texture, land cover, ...).
|
|
54
|
+
Edges connect pixels that are *both* spatially close *and* phase-correlated.
|
|
55
|
+
A **Graph Attention Network (GAT)** then learns *which neighbors to trust*,
|
|
56
|
+
propagating evidence of mechanical stability across structures while
|
|
57
|
+
attention weights automatically down-weight "false neighbors" β vegetation,
|
|
58
|
+
layover, or other superficially-correlated but genuinely unstable pixels.
|
|
59
|
+
|
|
60
|
+
We don't just claim this works β see [Validated results](#validated-results-on-synthetic-data) below for the actual experiments.
|
|
61
|
+
|
|
62
|
+
## Key features
|
|
63
|
+
|
|
64
|
+
- π§ **PS-GNN**: GCN β GAT (8-head, attention-exposing) β GraphSAGE
|
|
65
|
+
architecture with residual connections around the attention layer.
|
|
66
|
+
- π°οΈ **PS-ViT**: a spatio-temporal Vision Transformer baseline (patch
|
|
67
|
+
embedding + Bi-LSTM + Transformer) for ablation studies against the
|
|
68
|
+
graph-based approach.
|
|
69
|
+
- βοΈ **Physics-informed loss**: weighted cross-entropy + a *circular*
|
|
70
|
+
phase-stability term (correctly handles the Β±Ο wrap boundary, unlike a
|
|
71
|
+
naive linear standard deviation) + a spatial-coherence term that pulls
|
|
72
|
+
same-label neighbors together in embedding space.
|
|
73
|
+
- π― **Rigorous training**: 5-fold *spatial-block* cross-validation (not a
|
|
74
|
+
random split, which leaks on a spatial graph), curriculum learning over
|
|
75
|
+
the graph's phase-correlation threshold, AdamW + warmup/cosine schedule.
|
|
76
|
+
- π **Explainability & spatial statistics**: SHAP-based feature
|
|
77
|
+
attribution, Moran's I, Ripley's K-function, and uncertainty calibration
|
|
78
|
+
(reliability diagrams) out of the box.
|
|
79
|
+
- πΊοΈ **Rich reporting**: interactive Folium maps, 3D DEM-draped scatterer
|
|
80
|
+
clouds (Plotly), and a self-contained, gracefully-degrading HTML report
|
|
81
|
+
generator.
|
|
82
|
+
- π **Production-ready inference**: tiled processing for scenes too large
|
|
83
|
+
for a single graph (with a provably gap-free, non-overlapping stitching
|
|
84
|
+
scheme), genuine Monte Carlo Dropout uncertainty, ONNX export, and a
|
|
85
|
+
GPU β CPU β OpenVINO inference-runtime fallback chain.
|
|
86
|
+
- β
**SBAS validation**: a from-scratch weighted-least-squares SBAS
|
|
87
|
+
time-series inversion that checks whether PS-GNN's selections actually
|
|
88
|
+
produce *better deformation time series* than a classical ADI baseline β
|
|
89
|
+
not just better classification metrics.
|
|
90
|
+
- π **A full, executed Jupyter notebook** and a scientifically-hardened
|
|
91
|
+
synthetic stress-test script β not toy examples, but the actual
|
|
92
|
+
experiments behind the results below.
|
|
93
|
+
|
|
94
|
+
## Architecture
|
|
95
|
+
|
|
96
|
+
```mermaid
|
|
97
|
+
flowchart LR
|
|
98
|
+
subgraph Data["Data & Preprocessing"]
|
|
99
|
+
A[Sentinel-1 SLC stack] --> B["Node features (19-D)\ncompute_node_features"]
|
|
100
|
+
B --> C["Graph construction\nspatial + phase-correlation edges"]
|
|
101
|
+
end
|
|
102
|
+
subgraph Model["PS-GNN"]
|
|
103
|
+
C --> D["GCNConv\n+ BatchNorm"]
|
|
104
|
+
D --> E["GATConv (8 heads)\nβ attention weights"]
|
|
105
|
+
E --> F[SAGEConv]
|
|
106
|
+
F --> G[MLP classifier]
|
|
107
|
+
end
|
|
108
|
+
subgraph Train["Training"]
|
|
109
|
+
G --> H["PhysicsInformedPSLoss\nCE + phase stability + spatial coherence"]
|
|
110
|
+
H --> I["Spatial-block CV\n+ curriculum learning"]
|
|
111
|
+
end
|
|
112
|
+
subgraph Deploy["Inference & Validation"]
|
|
113
|
+
I --> J["Tiled inference\n+ MC Dropout uncertainty"]
|
|
114
|
+
J --> K[SBAS validation]
|
|
115
|
+
J --> L["Explainability\n+ spatial statistics"]
|
|
116
|
+
J --> M[HTML report]
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Installation
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install -e ".[dev,geo-extra]"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`ps-gnn` targets Python 3.10+ and depends on PyTorch and PyTorch
|
|
127
|
+
Geometric. GPU acceleration is optional but recommended for training on
|
|
128
|
+
real scenes.
|
|
129
|
+
|
|
130
|
+
## Quickstart
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import numpy as np
|
|
134
|
+
import torch
|
|
135
|
+
from torch_geometric.data import Data
|
|
136
|
+
|
|
137
|
+
from ps_gnn.data.label_generation import inject_synthetic_ps
|
|
138
|
+
from ps_gnn.data.preprocessing import GraphConstructionConfig, build_graph, compute_node_features
|
|
139
|
+
from ps_gnn.models.ps_gnn import PSGNN
|
|
140
|
+
from ps_gnn.training.trainer import Trainer, TrainerConfig
|
|
141
|
+
from ps_gnn.inference.detector import PSDetector, TilingConfig
|
|
142
|
+
|
|
143
|
+
# 1. Synthetic amplitude/phase stack (swap in a real Sentinel-1 stack here)
|
|
144
|
+
rng = np.random.default_rng(42)
|
|
145
|
+
amplitude = rng.gamma(2.0, 10.0, size=(20, 96, 96)).astype(np.float32)
|
|
146
|
+
phase = rng.uniform(-np.pi, np.pi, size=(20, 96, 96)).astype(np.float32)
|
|
147
|
+
result = inject_synthetic_ps(amplitude, phase, n_ps=50, random_state=42)
|
|
148
|
+
|
|
149
|
+
# 2. Feature engineering + graph construction
|
|
150
|
+
worldcover = np.full((96, 96), 30, dtype=np.int32)
|
|
151
|
+
features = compute_node_features(result.amplitude, result.phase, 38.0, worldcover)
|
|
152
|
+
config = GraphConstructionConfig(max_distance_m=20.0, min_phase_correlation=0.4)
|
|
153
|
+
# ... build node coordinates, call build_graph(), assemble a PyG Data object ...
|
|
154
|
+
|
|
155
|
+
# 3. Train
|
|
156
|
+
model = PSGNN()
|
|
157
|
+
trainer = Trainer(model, TrainerConfig(n_epochs=25))
|
|
158
|
+
# history = trainer.fit(data)
|
|
159
|
+
|
|
160
|
+
# 4. Tiled inference with uncertainty
|
|
161
|
+
detector = PSDetector(model, TilingConfig(tile_size=64, overlap=16), config)
|
|
162
|
+
# results = detector.detect(result.amplitude, result.phase, incidence_angle=38.0)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
For the **complete, runnable, end-to-end pipeline** β data generation through
|
|
166
|
+
training, the attention diagnostic, SHAP, spatial statistics, SBAS
|
|
167
|
+
validation, interactive maps, and an HTML report β see:
|
|
168
|
+
|
|
169
|
+
- π **[`ps_gnn_full_pipeline.ipynb`](ps_gnn_full_pipeline.ipynb)** β a fully
|
|
170
|
+
executed Jupyter notebook, runnable top-to-bottom in a few minutes.
|
|
171
|
+
- π¬ **[`run_synthetic_pipeline_hardened.py`](run_synthetic_pipeline_hardened.py)** β
|
|
172
|
+
the larger-scale (128Γ128, 50-epoch) version of the same experiment, as a
|
|
173
|
+
standalone script.
|
|
174
|
+
|
|
175
|
+
## Validated results on synthetic data
|
|
176
|
+
|
|
177
|
+
These are real numbers from actual runs (see the notebook and hardened
|
|
178
|
+
script above), not illustrative placeholders β including a result that
|
|
179
|
+
went against the initial hypothesis, reported honestly rather than omitted.
|
|
180
|
+
|
|
181
|
+
| Experiment | Result |
|
|
182
|
+
|---|---|
|
|
183
|
+
| **Attention mechanism** β does the GAT layer learn to down-weight a deceptive "false neighbor" (a pixel graph-connected to a true PS via correlated phase, but with unstable amplitude) below a generic background edge? | **Confirmed.** Mean attention on false-neighbor edges is measurably lower than on backgroundβbackground edges, and the gap widens with more training data. |
|
|
184
|
+
| **Borderline-ADI recovery** β can PS-GNN recover genuine scatterers whose *individual* ADI exceeds the classical 0.25 threshold, using only mutual phase corroboration within a cluster? | **Confirmed, large effect.** PS-GNN: 97.5% recall vs. a fixed ADI<0.25 baseline: 10.0% recall on the same points. |
|
|
185
|
+
| **Aggregate SBAS coherence vs. classical ADI thresholding** | **Not met** in the hardened configuration β a real trade-off where recovering borderline points costs some easy-point recall, which nets out negatively in the aggregate metric. Reported honestly; see the notebook's Section 11 discussion. |
|
|
186
|
+
|
|
187
|
+
The borderline-ADI experiment is the one that matters most: it's the first
|
|
188
|
+
test in this project specifically engineered so classical thresholding
|
|
189
|
+
*cannot* win by construction, and PS-GNN's large, unambiguous margin there
|
|
190
|
+
is the clearest evidence that graph reasoning is doing something a
|
|
191
|
+
per-pixel method structurally cannot.
|
|
192
|
+
|
|
193
|
+
## Project layout
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
ps_gnn/
|
|
197
|
+
βββ data/ # Fetching, ground-truth / pseudo-label generation, preprocessing, graph construction
|
|
198
|
+
βββ models/ # PS-GNN, PS-ViT, physics-informed losses
|
|
199
|
+
βββ training/ # Trainer (spatial CV, curriculum learning), ablation runner
|
|
200
|
+
βββ inference/ # Tiled inference, Monte Carlo uncertainty
|
|
201
|
+
βββ validation/ # From-scratch SBAS time-series inversion & validation
|
|
202
|
+
βββ analytics/ # Explainability (SHAP), spatial statistics, HTML report generator
|
|
203
|
+
βββ visualization/ # Interactive maps (Folium) and charts (Plotly)
|
|
204
|
+
βββ utils/ # Deployment: ONNX export, weight caching, runtime fallback
|
|
205
|
+
tests/ # Unit, model, integration, and validation tests (42 tests)
|
|
206
|
+
docs/ # Documentation index and logo assets
|
|
207
|
+
synthetic_pipeline_utils.py # Shared synthetic-data injection & diagnostic helpers
|
|
208
|
+
run_synthetic_pipeline_hardened.py # Full-scale stress-test script (see results above)
|
|
209
|
+
ps_gnn_full_pipeline.ipynb # Complete, executed end-to-end notebook
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Documentation
|
|
213
|
+
|
|
214
|
+
A module-by-module map, tying every pipeline stage to its source file, is
|
|
215
|
+
in [`docs/index.md`](docs/index.md). Every public function has a complete
|
|
216
|
+
NumPy-style docstring β start there for API details.
|
|
217
|
+
|
|
218
|
+
## Testing
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# Fast unit/model tests only
|
|
222
|
+
pytest tests/ -m "not slow"
|
|
223
|
+
|
|
224
|
+
# Full suite, including integration tests
|
|
225
|
+
pytest tests/
|
|
226
|
+
|
|
227
|
+
# With coverage
|
|
228
|
+
pytest tests/ --cov=ps_gnn --cov-report=term-missing
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
CI (`.github/workflows/ci.yml`) runs `ruff` + `black`, a fast-test matrix
|
|
232
|
+
across Python 3.10β3.12, and a separate slow/integration job. See
|
|
233
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md) for local dev setup and coding
|
|
234
|
+
conventions.
|
|
235
|
+
|
|
236
|
+
## Status
|
|
237
|
+
|
|
238
|
+
This package is under active development as part of a Copernicus Master's
|
|
239
|
+
research project on AI-assisted InSAR processing. APIs may change between
|
|
240
|
+
minor versions until `1.0`. See [`CHANGELOG.md`](CHANGELOG.md) for release
|
|
241
|
+
history.
|
|
242
|
+
|
|
243
|
+
## Citation
|
|
244
|
+
|
|
245
|
+
If you use `ps-gnn` in academic work, please cite this repository using the
|
|
246
|
+
metadata in [`CITATION.cff`](CITATION.cff) (a paper citation will be added
|
|
247
|
+
once the associated research is published).
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT β see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ps-gnn: AI-driven Persistent Scatterer identification for InSAR time series.
|
|
3
|
+
|
|
4
|
+
ps-gnn replaces the classical, per-pixel Amplitude Dispersion Index (ADI)
|
|
5
|
+
threshold with a Graph Attention Network (GAT) that reasons over the
|
|
6
|
+
*spatial relationships* between candidate scatterers. By propagating
|
|
7
|
+
information across a graph built from spatial proximity and phase
|
|
8
|
+
correlation, the model learns to down-weight noisy "false neighbors"
|
|
9
|
+
(e.g. vegetation, layover/shadow) and to up-weight mechanically stable,
|
|
10
|
+
coherent structures (buildings, infrastructure, exposed rock).
|
|
11
|
+
|
|
12
|
+
This top-level package intentionally stays lightweight: it re-exports the
|
|
13
|
+
package version and leaves heavy imports (torch, torch_geometric, rasterio,
|
|
14
|
+
...) to the submodules that actually need them, so that ``import ps_gnn``
|
|
15
|
+
stays fast and side-effect free.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
__all__ = ["__version__"]
|
|
File without changes
|