Skip to main content
ps-gnn

⚠️ Important Note

This project is actively under development. While the core functionality is production-ready and thoroughly tested, some advanced features are still being refined.

License: MIT Python 3.10+ CI Tests Version Ruff PRs welcome

Graph Attention Networks for Persistent Scatterer identification in InSAR time series.
Reframing classical PSI point selection as node classification on a spatial graph, so the model reasons about which neighbors to trust instead of judging every pixel in isolation.


ps-gnn is a standalone, open-source Python package for AI-driven Persistent Scatterer (PS) identification in multi-temporal InSAR (Interferometric Synthetic Aperture Radar) stacks. It is designed as a companion to pygeofetch but works entirely on its own with any Sentinel-1 SLC-derived amplitude/phase stack — including purely synthetic data, which is how this README's own results were produced.

Why a graph, not a threshold?

Classical PS selection (StaMPS, SqueeSAR, and most operational pipelines) relies on the Amplitude Dispersion Index (ADI) — a per-pixel statistic that ignores everything happening around the pixel. Two consequences:

  • Isolated noisy pixels with low ADI by chance are false positives.
  • Weakly stable but spatially corroborated pixels (e.g. a scatterer whose individual ADI sits just above the classical cutoff, but which sits in a small cluster of mutually phase-correlated neighbors) are missed outright, no matter how good the surrounding evidence is.

ps-gnn reframes PS selection as node classification on a spatial graph. Each candidate pixel is a node with a 19-D feature vector (amplitude statistics, temporal coherence, local texture, land cover, ...). Edges connect pixels that are both spatially close and phase-correlated. A Graph Attention Network (GAT) then learns which neighbors to trust, propagating evidence of mechanical stability across structures while attention weights automatically down-weight "false neighbors" — vegetation, layover, or other superficially-correlated but genuinely unstable pixels.

We don't just claim this works — see Validated results below for the actual experiments.

Key features

  • 🧠 PS-GNN: GCN → GAT (8-head, attention-exposing) → GraphSAGE architecture with residual connections around the attention layer.
  • 🛰️ PS-ViT: a spatio-temporal Vision Transformer baseline (patch embedding + Bi-LSTM + Transformer) for ablation studies against the graph-based approach.
  • ⚛️ Physics-informed loss: weighted cross-entropy + a circular phase-stability term (correctly handles the ±π wrap boundary, unlike a naive linear standard deviation) + a spatial-coherence term that pulls same-label neighbors together in embedding space.
  • 🎯 Rigorous training: 5-fold spatial-block cross-validation (not a random split, which leaks on a spatial graph), curriculum learning over the graph's phase-correlation threshold, AdamW + warmup/cosine schedule.
  • 📊 Explainability & spatial statistics: SHAP-based feature attribution, Moran's I, Ripley's K-function, and uncertainty calibration (reliability diagrams) out of the box.
  • 🗺️ Rich reporting: interactive Folium maps, 3D DEM-draped scatterer clouds (Plotly), and a self-contained, gracefully-degrading HTML report generator.
  • 🚀 Production-ready inference: tiled processing for scenes too large for a single graph (with a provably gap-free, non-overlapping stitching scheme), genuine Monte Carlo Dropout uncertainty, ONNX export, and a GPU → CPU → OpenVINO inference-runtime fallback chain.
  • SBAS validation: a from-scratch weighted-least-squares SBAS time-series inversion that checks whether PS-GNN's selections actually produce better deformation time series than a classical ADI baseline — not just better classification metrics.
  • 📓 A full, executed Jupyter notebook and a scientifically-hardened synthetic stress-test script — not toy examples, but the actual experiments behind the results below.

Architecture

flowchart LR
    subgraph Data["Data & Preprocessing"]
        A[Sentinel-1 SLC stack] --> B["Node features (19-D)\ncompute_node_features"]
        B --> C["Graph construction\nspatial + phase-correlation edges"]
    end
    subgraph Model["PS-GNN"]
        C --> D["GCNConv\n+ BatchNorm"]
        D --> E["GATConv (8 heads)\n← attention weights"]
        E --> F[SAGEConv]
        F --> G[MLP classifier]
    end
    subgraph Train["Training"]
        G --> H["PhysicsInformedPSLoss\nCE + phase stability + spatial coherence"]
        H --> I["Spatial-block CV\n+ curriculum learning"]
    end
    subgraph Deploy["Inference & Validation"]
        I --> J["Tiled inference\n+ MC Dropout uncertainty"]
        J --> K[SBAS validation]
        J --> L["Explainability\n+ spatial statistics"]
        J --> M[HTML report]
    end

Installation

pip install -e ".[dev,geo-extra]"

ps-gnn targets Python 3.10+ and depends on PyTorch and PyTorch Geometric. GPU acceleration is optional but recommended for training on real scenes.

Quickstart

import numpy as np
import torch
from torch_geometric.data import Data

from ps_gnn.data.label_generation import inject_synthetic_ps
from ps_gnn.data.preprocessing import GraphConstructionConfig, build_graph, compute_node_features
from ps_gnn.models.ps_gnn import PSGNN
from ps_gnn.training.trainer import Trainer, TrainerConfig
from ps_gnn.inference.detector import PSDetector, TilingConfig

# 1. Synthetic amplitude/phase stack (swap in a real Sentinel-1 stack here)
rng = np.random.default_rng(42)
amplitude = rng.gamma(2.0, 10.0, size=(20, 96, 96)).astype(np.float32)
phase = rng.uniform(-np.pi, np.pi, size=(20, 96, 96)).astype(np.float32)
result = inject_synthetic_ps(amplitude, phase, n_ps=50, random_state=42)

# 2. Feature engineering + graph construction
worldcover = np.full((96, 96), 30, dtype=np.int32)
features = compute_node_features(result.amplitude, result.phase, 38.0, worldcover)
config = GraphConstructionConfig(max_distance_m=20.0, min_phase_correlation=0.4)
# ... build node coordinates, call build_graph(), assemble a PyG Data object ...

# 3. Train
model = PSGNN()
trainer = Trainer(model, TrainerConfig(n_epochs=25))
# history = trainer.fit(data)

# 4. Tiled inference with uncertainty
detector = PSDetector(model, TilingConfig(tile_size=64, overlap=16), config)
# results = detector.detect(result.amplitude, result.phase, incidence_angle=38.0)

For the complete, runnable, end-to-end pipeline — data generation through training, the attention diagnostic, SHAP, spatial statistics, SBAS validation, interactive maps, and an HTML report — see:

Validated results on synthetic data

These are real numbers from actual runs (see the notebook and hardened script above), not illustrative placeholders — including a result that went against the initial hypothesis, reported honestly rather than omitted.

Experiment Result
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.
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.
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.

The borderline-ADI experiment is the one that matters most: it's the first test in this project specifically engineered so classical thresholding cannot win by construction, and PS-GNN's large, unambiguous margin there is the clearest evidence that graph reasoning is doing something a per-pixel method structurally cannot.

Project layout

ps_gnn/
├── data/            # Fetching, ground-truth / pseudo-label generation, preprocessing, graph construction
├── models/          # PS-GNN, PS-ViT, physics-informed losses
├── training/        # Trainer (spatial CV, curriculum learning), ablation runner
├── inference/       # Tiled inference, Monte Carlo uncertainty
├── validation/      # From-scratch SBAS time-series inversion & validation
├── analytics/       # Explainability (SHAP), spatial statistics, HTML report generator
├── visualization/   # Interactive maps (Folium) and charts (Plotly)
└── utils/           # Deployment: ONNX export, weight caching, runtime fallback
tests/               # Unit, model, integration, and validation tests (42 tests)
docs/                # Documentation index and logo assets
synthetic_pipeline_utils.py       # Shared synthetic-data injection & diagnostic helpers
run_synthetic_pipeline_hardened.py # Full-scale stress-test script (see results above)
ps_gnn_full_pipeline.ipynb        # Complete, executed end-to-end notebook

Documentation

A module-by-module map, tying every pipeline stage to its source file, is in docs/index.md. Every public function has a complete NumPy-style docstring — start there for API details.

Testing

# Fast unit/model tests only
pytest tests/ -m "not slow"

# Full suite, including integration tests
pytest tests/

# With coverage
pytest tests/ --cov=ps_gnn --cov-report=term-missing

CI (.github/workflows/ci.yml) runs ruff + black, a fast-test matrix across Python 3.10–3.12, and a separate slow/integration job. See CONTRIBUTING.md for local dev setup and coding conventions.

Status

This package is under active development as part of a Copernicus Master's research project on AI-assisted InSAR processing. APIs may change between minor versions until 1.0. See CHANGELOG.md for release history.

Citation

If you use ps-gnn in academic work, please cite this repository using the metadata in CITATION.cff (a paper citation will be added once the associated research is published).

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ps_gnn-0.1.0.tar.gz (85.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ps_gnn-0.1.0-py3-none-any.whl (83.3 kB view details)

Uploaded Python 3

File details

Details for the file ps_gnn-0.1.0.tar.gz.

File metadata

  • Download URL: ps_gnn-0.1.0.tar.gz
  • Upload date:
  • Size: 85.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for ps_gnn-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5492bb35a97b179199cf01863256abdaf6bfe45c3a7759655b9ebd3812f2d104
MD5 a1e43ab8c30b74fdd01d9f53530f620a
BLAKE2b-256 3347049e49fe1e4fc7910761cbecdae9e2fe9e0644625e36b48f6367b62fe6e1

See more details on using hashes here.

File details

Details for the file ps_gnn-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: ps_gnn-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 83.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for ps_gnn-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e196792e0a910278366fd8f632c9ae19fadab002afc74a0db889bb24162508c4
MD5 5cc86b7001ac3971affb3522cac65ca4
BLAKE2b-256 67cafc2fe446995624f9e4c36843ebc6eb7c65fa56212c9b11a1d2090253888f

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page