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.
Files changed (41) hide show
  1. ps_gnn-0.1.0/LICENSE +21 -0
  2. ps_gnn-0.1.0/PKG-INFO +302 -0
  3. ps_gnn-0.1.0/README.md +251 -0
  4. ps_gnn-0.1.0/ps_gnn/__init__.py +21 -0
  5. ps_gnn-0.1.0/ps_gnn/analytics/__init__.py +0 -0
  6. ps_gnn-0.1.0/ps_gnn/analytics/explainability.py +305 -0
  7. ps_gnn-0.1.0/ps_gnn/analytics/report_generator.py +361 -0
  8. ps_gnn-0.1.0/ps_gnn/analytics/spatial_stats.py +382 -0
  9. ps_gnn-0.1.0/ps_gnn/data/__init__.py +0 -0
  10. ps_gnn-0.1.0/ps_gnn/data/fetch_benchmarks.py +308 -0
  11. ps_gnn-0.1.0/ps_gnn/data/label_generation.py +375 -0
  12. ps_gnn-0.1.0/ps_gnn/data/preprocessing.py +578 -0
  13. ps_gnn-0.1.0/ps_gnn/inference/__init__.py +0 -0
  14. ps_gnn-0.1.0/ps_gnn/inference/detector.py +444 -0
  15. ps_gnn-0.1.0/ps_gnn/models/__init__.py +0 -0
  16. ps_gnn-0.1.0/ps_gnn/models/losses.py +332 -0
  17. ps_gnn-0.1.0/ps_gnn/models/ps_gnn.py +269 -0
  18. ps_gnn-0.1.0/ps_gnn/models/ps_vit.py +270 -0
  19. ps_gnn-0.1.0/ps_gnn/py.typed +0 -0
  20. ps_gnn-0.1.0/ps_gnn/training/__init__.py +0 -0
  21. ps_gnn-0.1.0/ps_gnn/training/ablation.py +500 -0
  22. ps_gnn-0.1.0/ps_gnn/training/trainer.py +463 -0
  23. ps_gnn-0.1.0/ps_gnn/utils/__init__.py +0 -0
  24. ps_gnn-0.1.0/ps_gnn/utils/deployment.py +392 -0
  25. ps_gnn-0.1.0/ps_gnn/validation/__init__.py +0 -0
  26. ps_gnn-0.1.0/ps_gnn/validation/sbas_check.py +370 -0
  27. ps_gnn-0.1.0/ps_gnn/visualization/__init__.py +0 -0
  28. ps_gnn-0.1.0/ps_gnn/visualization/charts.py +235 -0
  29. ps_gnn-0.1.0/ps_gnn/visualization/maps.py +305 -0
  30. ps_gnn-0.1.0/ps_gnn.egg-info/PKG-INFO +302 -0
  31. ps_gnn-0.1.0/ps_gnn.egg-info/SOURCES.txt +39 -0
  32. ps_gnn-0.1.0/ps_gnn.egg-info/dependency_links.txt +1 -0
  33. ps_gnn-0.1.0/ps_gnn.egg-info/entry_points.txt +2 -0
  34. ps_gnn-0.1.0/ps_gnn.egg-info/requires.txt +31 -0
  35. ps_gnn-0.1.0/ps_gnn.egg-info/top_level.txt +1 -0
  36. ps_gnn-0.1.0/pyproject.toml +100 -0
  37. ps_gnn-0.1.0/setup.cfg +4 -0
  38. ps_gnn-0.1.0/tests/test_model.py +261 -0
  39. ps_gnn-0.1.0/tests/test_pipeline.py +188 -0
  40. ps_gnn-0.1.0/tests/test_preprocessing.py +202 -0
  41. 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