Metadata-Version: 2.4
Name: pyunwrap-insar
Version: 0.1.3
Summary: AI-driven InSAR phase unwrapping via a Physics-Informed U-Net (Ambiguity-Net) that predicts the discrete integer ambiguity map, enforcing the rewrapping constraint by construction.
Author: Samuel Appiah Kubi
License: MIT
Project-URL: Homepage, https://github.com/EOCoreINT/pyunwrap
Project-URL: Repository, https://github.com/EOCoreINT/pyunwrap
Project-URL: Issues, https://github.com/EOCoreINT/pyunwrap/issues
Keywords: InSAR,phase-unwrapping,SAR,remote-sensing,deep-learning,earth-observation,geospatial
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: torch>=2.1
Requires-Dist: torchvision
Requires-Dist: rasterio>=1.3
Requires-Dist: geopandas>=0.14
Requires-Dist: numpy>=1.24
Requires-Dist: scikit-image>=0.22
Requires-Dist: scikit-learn>=1.3
Requires-Dist: shap>=0.44
Requires-Dist: plotly>=5.18
Requires-Dist: pyvista>=0.43
Requires-Dist: jinja2>=3.1
Requires-Dist: onnxruntime>=1.16
Requires-Dist: scipy>=1.11
Requires-Dist: tqdm>=4.66
Requires-Dist: pandas>=2.1
Requires-Dist: tensorboard>=2.15
Requires-Dist: h5py>=3.10
Provides-Extra: dev
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: pytest-cov>=4.1; extra == "dev"
Requires-Dist: black>=23.12; extra == "dev"
Requires-Dist: ruff>=0.1.9; extra == "dev"
Requires-Dist: mypy>=1.7; extra == "dev"
Provides-Extra: maps
Requires-Dist: folium>=0.15; extra == "maps"
Requires-Dist: leafmap>=0.30; extra == "maps"
Provides-Extra: deploy
Requires-Dist: openvino>=2023.2; extra == "deploy"
Requires-Dist: onnx>=1.16; extra == "deploy"
Requires-Dist: onnxconverter-common>=1.14; extra == "deploy"
Requires-Dist: onnxscript>=0.1; extra == "deploy"
Provides-Extra: notebooks
Requires-Dist: jupyter>=1.0; extra == "notebooks"
Requires-Dist: ipykernel>=6.29; extra == "notebooks"
Provides-Extra: snaphu
Requires-Dist: snaphu>=0.4.0; extra == "snaphu"
Provides-Extra: docs
Requires-Dist: sphinx>=7.0; extra == "docs"
Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
Requires-Dist: myst-parser>=2.0; extra == "docs"
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
Requires-Dist: sphinx-autodoc-typehints>=1.25; extra == "docs"
Requires-Dist: sphinx-design>=0.5; extra == "docs"
Requires-Dist: nbsphinx>=0.9; extra == "docs"
Provides-Extra: all
Requires-Dist: folium>=0.15; extra == "all"
Requires-Dist: leafmap>=0.30; extra == "all"
Requires-Dist: openvino>=2023.2; extra == "all"
Requires-Dist: onnx>=1.16; extra == "all"
Requires-Dist: onnxconverter-common>=1.14; extra == "all"
Requires-Dist: onnxscript>=0.1; extra == "all"
Requires-Dist: jupyter>=1.0; extra == "all"
Requires-Dist: ipykernel>=6.29; extra == "all"
Dynamic: license-file

<div align="center">

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="assets/logo-horizontal-dark.svg">
  <img src="assets/logo-horizontal.svg" alt="pyunwrap" width="420">
</picture>

**InSAR phase unwrapping by predicting the integer ambiguity: the wrap identity holds by construction.**

[![PyPI](https://img.shields.io/pypi/v/pyunwrap-insar.svg)](https://pypi.org/project/pyunwrap-insar/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](pyproject.toml)
[![Tests: 235 passing](https://img.shields.io/badge/tests-235%20passing-brightgreen)](tests/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Docs](https://readthedocs.org/projects/pyunwrap/badge/?version=latest)](https://pyunwrap.readthedocs.io/en/latest/?badge=latest)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22209219.svg)](https://doi.org/10.5281/zenodo.22209219)
[![Status: early-stage](https://img.shields.io/badge/status-early--stage-orange)](#project-status)

[Why this exists](#why-this-exists) ·
[How it works](#how-it-works) ·
[Synthetic data](#synthetic-data-engine) ·
[Curriculum](#staged-curriculum-training) ·
[Honest evaluation](#how-we-judge-a-model-honestly) ·
[Hybrid unwrapping](#hybrid-unwrapping) ·
[Install](#installation) ·
[Quickstart](#quickstart) ·
[Notebooks](#notebooks) ·
[Docs](https://pyunwrap.readthedocs.io) ·
[Citation](#citation)

</div>

---

`pyunwrap` is a research package for unwrapping Interferometric Synthetic
Aperture Radar (InSAR) phase, the core measurement behind satellite-based
ground-deformation monitoring. A deep network (`AmbiguityNet`) predicts the
**integer number of phase cycles** at every pixel, and the unwrapped phase is
rebuilt from it. Because of that design, the output always re-wraps to exactly
the observed interferogram.

![Synthetic interferograms from three deformation models: rainbow fringes, SAR intensity, coherence-weighted phase and the true ground motion](notebooks/08_advanced_pipeline/standard/figures/01_interferogram_gallery_clean.png)

*Synthetic C-band interferograms from the package's generator (Gaussian bowl,
Okada fault, Mogi volcano, and a no-deformation control). One colour cycle is
one fringe, 2.77 cm of line-of-sight motion. Bottom row: the true ground
motion the network never sees.*

> [!IMPORTANT]
> **Status in one paragraph.** The pipeline is implemented and tested end to
> end, but the model is **not yet a replacement for SNAPHU**, and the claim in
> earlier versions of this README that the learned model "wins in low
> coherence" has been **withdrawn**: a trivial *do-nothing* predictor
> (`k = 0` everywhere) reproduces those wins, so they did not show learning.
> Details and the current numbers are under
> [How we judge a model honestly](#how-we-judge-a-model-honestly) and
> [Project status](#project-status).

## Why this exists

Every classical phase-unwrapping algorithm (branch-cut methods,
minimum-cost flow as in SNAPHU, Goldstein's algorithm) rests on one
assumption: the true phase gradient between adjacent pixels never exceeds `π`
radians (the Nyquist/Itoh condition). Two situations break it in practice:

- **Low coherence.** Vegetation, water and temporal decorrelation add
  near-random phase noise. Once the local gradient is unreliable, branch-cut
  and MCF methods propagate the error across the connected region downstream.
- **Steep deformation gradients.** Earthquakes, volcanic inflation and mining
  subsidence can produce genuine gradients above `π` radians per pixel near
  the source. This cannot be recovered from the wrapped phase alone without a
  prior, which is the gap a learned model could in principle fill.

`pyunwrap` targets both regimes without inheriting the most common failure
mode of naive deep-learning approaches: regressing the unwrapped phase gives a
network no reason to respect `wrap(prediction) == observed_phase`. Predicting
the integer ambiguity makes that identity a mathematical guarantee.

## How it works

`AmbiguityNet` outputs a continuous ambiguity prediction, rounds it with a
straight-through estimator, and reconstructs phase directly:

```
φ̂ = ψ + 2π · round(k̂),   k̂ ∈ ℤ
```

| Approach | Failure mode |
|---|---|
| Regress `φ` directly | Nothing constrains `wrap(prediction) == ψ`; the network can output any real value |
| **Predict `k` (this package)** | `wrap(ψ + 2π·round(k̂)) == ψ` holds by construction; the network only has to get the *integer cycle count* right |

![Observed interferogram, the re-wrapped prediction and their difference, at the 1e-16 rad floating-point level](notebooks/08_advanced_pipeline/standard/figures/20_physics_identity_rainbow.png)

*The physics identity in action: re-wrapping the predicted unwrapped phase
reproduces the observed interferogram to 8.9e-16 rad. This figure shows that
the output is **self-consistent**, which is guaranteed by the design. It says
nothing about whether the cycle counts are **correct**; that is what the
evaluation below is for.*

Two consequences shape the rest of the code: training uses a physics-informed
loss with a component that is provably near-zero regardless of prediction
quality (documented explicitly in
[`pyunwrap/models/losses.py`](pyunwrap/models/losses.py)), and tiled inference
merges the **integer ambiguity map** across overlapping patches, never the
phase itself, because averaging phase across a tile boundary can silently
produce a value that satisfies no physical interferogram.

## Synthetic data engine

[`pyunwrap.synthetic`](pyunwrap/synthetic/generator.py) generates
interferograms with exactly known ground truth: Gaussian subsidence bowls, a
from-scratch Okada (1985) rectangular-fault dislocation, a Mogi (1958)
volcanic point source, DEM-driven topographic phase, Kolmogorov-spectrum
atmospheric turbulence, orbital ramps, and coherence-dependent decorrelation
noise. A pseudo-real strategy rewraps real L-band (ALOS-2) unwrapped phase
into simulated C-band data to help bridge the sim-to-real gap.

![Anatomy of a composite scene: three deformation sources, nuisance phase, coherence with patches and lakes, and the final interferogram](notebooks/08_advanced_pipeline/standard/figures/07_scene_anatomy.png)

*Anatomy of one composite training scene: three sources are summed, then
topography, atmosphere and an orbital ramp are added, coherence is degraded by
patches, lakes and steep fringes, and the result is wrapped.*

The advanced notebook builds composite scenes with domain randomisation (X, C
and L band, incidence angle, heading, ±25 % parameter jitter) under an
**ambiguity budget**: every scene keeps `max |k| ≤ 8`, matching the output
range of the network, so no label is unreachable by construction.

## Staged curriculum training

Training starts on clean, wrapped scenes and adds difficulty in stages. The
easy and moderate bowls are sized in **fringes**, not metres, so every scene
is guaranteed to wrap in every band (an early version used a fixed 2 cm bowl,
which barely wrapped at C-band and almost never in L-band, so the "easy" tier
taught nothing but `k = 0`).

![Easy, moderate and hard composite scenes in C and L band: rainbow interferogram, coherence view, true unwrapped phase and true integer ambiguity](notebooks/08_advanced_pipeline/standard/figures/06_composite_gallery.png)

*The three stages in C and L band. Easy: one source, coherence 0.95, no
atmosphere. Moderate: two sources, coherence 0.70. Hard: three sources,
coherence ≈ 0.4, lakes, patches, steep fringes (Nyquist violations ≈ 30 %).
Bottom row: the integer ambiguity map `k` the network must predict.*

| Stage | Starts | Sources | Coherence | Nuisance |
|---|---|---|---|---|
| Easy | epoch 1 | bowl (2–4 fringes) | 0.95 | none |
| Moderate | epoch 5 | bowl (3–6 fringes) + deep Okada fault | 0.70 | mild atmosphere |
| Hard | epoch 10 | bowl + shallow Okada + Mogi | 0.40 | patches, lakes, strong atmosphere, ramp, baseline |

The library also provides **curriculum replay**
([`pyunwrap.training.curriculum`](pyunwrap/training/curriculum.py)): every
epoch draws a fixed-proportion mixture of easy/moderate/hard tiles, with
automatic forgetting detection. Optional SNAPHU pseudo-ground-truth
fine-tuning on real data is supported through
[`pyunwrap.utils.snaphu_integration`](pyunwrap/utils/snaphu_integration.py).

## How we judge a model honestly

Two facts make naive evaluation misleading, and the package's reporting is
built around them.

**1. The absolute cycle count is unobservable.** Shifting a whole scene by *n*
cycles leaves the wrapped phase unchanged, so no model can know the absolute
level. Every score is therefore computed **after removing each tile's
whole-cycle offset**, and the advanced notebook trains with an
offset-invariant loss (and checks numerically that the loss is unchanged when
the target is shifted by a constant).

**2. A model must beat "do nothing".** Predicting `k = 0` everywhere is
already right on a large share of pixels in gentle scenes. The headline
metric is therefore **skill**:

```
skill = 1 − RMSE(model) / RMSE(do-nothing)        (offset-corrected, in radians)
```

`0 %` means no better than guessing zero; `100 %` is perfect; negative is
worse. The notebook also reports a per-stage **ceiling** (the skill of an
oracle that knows the noise-free cycle count) so a number can be read against
what is achievable.

> [!NOTE]
> Earlier benchmarks in [`docs/experiments.md`](docs/experiments.md) compared
> only against SNAPHU and a scikit-image fallback and had no do-nothing
> column. Re-checking them, the "learned model wins at low coherence" result
> is matched by the do-nothing predictor, scene by scene. Treat those
> benchmark conclusions as **superseded** until they are re-run with a
> do-nothing baseline.

## Hybrid unwrapping

[`pyunwrap.inference.hybrid.HybridUnwrapper`](pyunwrap/inference/hybrid.py)
routes each pixel to a classical or a learned method by local regime:

| Regime | Routed to | Intended reason |
|---|---|---|
| High coherence, low gradient | Classical (SNAPHU, or a scikit-image fallback) | Mature, fast, reliable here |
| Low coherence / high decorrelation | Learned model | Where classical cost models are least reliable |
| High coherence **and** high gradient (optional) | Confidence-weighted blend | Steep gradients can violate Nyquist even at high coherence |

> [!WARNING]
> The routing rule for the low-coherence regime was motivated by the
> withdrawn benchmark result above. It is kept as a design option, not as a
> demonstrated advantage. Use `--no-hybrid` / the learned model alone, or
> classical alone, until a model that beats the do-nothing baseline in that
> regime exists.

The merge never averages raw phase. Both methods' results are converted to
integer ambiguity maps, blended *there*, and the final phase is rebuilt as
`wrapped_phase + 2π·round(k_merged)`, so the wrap identity holds exactly
regardless of how the two maps were merged.

```python
from pyunwrap.inference.hybrid import HybridUnwrapper

hybrid = HybridUnwrapper(learned_unwrapper)  # auto-selects SNAPHU or a scikit-image fallback
result = hybrid.unwrap("wrapped.tif", "coherence.tif", "amplitude.tif")
result.save_geotiff("unwrapped.tif")
# result.regime_mask / .provenance_mask / .confidence_map / .uncertainty_map
# are also available, so you can see which method produced which region.
```

[`pyunwrap.analytics.benchmark.generate_regime_report`](pyunwrap/analytics/benchmark.py)
reports which scenes each method wins, scene by scene, instead of one
aggregate score.

## Key features

**Real-data + synthetic-injection pipeline**
([`pyunwrap.data.real_injection`](pyunwrap/data/real_injection.py)): inject
synthetic deformation (Gaussian bowl, Mogi, Okada, orbital ramp or a
nonlinear transient) with exactly known ground truth into a real (or
real-like) wrapped-phase/amplitude/coherence stack, preserving real speckle
and decorrelation texture. Spatially-blocked splitting prevents pixel-level
leakage between train/val/test.

**`AmbiguityNet`** ([`pyunwrap.models`](pyunwrap/models/ambiguity_net.py)): a
ResNet-34-encoder U-Net (about 24 M parameters) with a dual head, the integer
ambiguity map via a straight-through rounding layer and an auxiliary
residue-probability map, plus `Dropout2d` layers for Monte Carlo Dropout
uncertainty. Trained with a physics-informed loss (ambiguity regression,
re-wrap consistency, ambiguity-map residue penalty, and an optional
coherence-weighted edge-preserving smoothness term).

**Production inference** ([`pyunwrap.inference`](pyunwrap/inference/unwrapper.py)):
tiled processing of arbitrarily large interferograms with edge-aware,
residue-probability-weighted merging of the ambiguity map, test-time
augmentation over the eight dihedral views (integer median), Monte Carlo
Dropout uncertainty, and an ONNX Runtime → OpenVINO fallback chain for
deployment without PyTorch.

**Scientific analytics** ([`pyunwrap.analytics`](pyunwrap/analytics/)):
residue detection and clustering, Nyquist gradient-violation mapping,
error-distribution statistics, Grad-CAM and Integrated-Gradients
explainability, uncertainty-calibration diagrams, and an offset-corrected,
regime-separated benchmark harness.

**Visualization & reporting** ([`pyunwrap.visualization`](pyunwrap/visualization/)):
interactive Plotly 3D phase surfaces and heatmaps, a folium swipe-comparison
map, and a self-contained HTML report.

## Architecture

```
InSARSyntheticGenerator ──┐                     real GeoTIFFs
                           │                     (wrapped, coherence, amplitude)
                           ▼                              │
                  tiling + normalization                  ▼
                           │                     tiled inference
                           ▼                     via PhaseUnwrapper
                  InSARTileDataset                         │
                           │                                │
                           ▼                                ▼
                   Trainer.fit()  ──── AmbiguityNet ──── smart ambiguity-map
                  (curriculum,          (the same           merging (never
                   physics loss)         model)              the phase)
                                                              │
                                                              ▼
                                                    unwrapped phase +
                                                    analytics + report
```

See [`docs/architecture.md`](docs/architecture.md) for the module-by-module
reference.

## Installation

From PyPI (distributed as `pyunwrap-insar`; the importable module is
`pyunwrap`):

```bash
pip install pyunwrap-insar
```

From source, for development:

```bash
git clone https://github.com/EOCoreINT/pyunwrap.git
cd pyunwrap
pip install -e .
```

Optional extras:

| Extra | Adds | Use case |
|---|---|---|
| `dev` | `pytest`, `pytest-cov`, `black`, `ruff`, `mypy` | Development, testing, linting |
| `maps` | `folium`, `leafmap` | Interactive map visualization |
| `deploy` | `openvino`, `onnx`, `onnxconverter-common`, `onnxscript` | ONNX export, OpenVINO inference |
| `notebooks` | `jupyter`, `ipykernel` | Running the example notebooks |
| `snaphu` | `snaphu` (isce-framework bindings) | SNAPHU pseudo-ground-truth fine-tuning, benchmarking |
| `docs` | `sphinx`, `sphinx-rtd-theme`, `myst-parser`, `nbsphinx`, ... | Building the [documentation site](https://pyunwrap.readthedocs.io) locally |

```bash
pip install -e ".[dev,maps,deploy,notebooks,snaphu]"   # everything, from source
pip install "pyunwrap-insar[maps,snaphu]"               # extras also work from PyPI
```

> [!TIP]
> On Python 3.14, PyTorch `DataLoader` workers default to the `forkserver`
> start method, which cannot pickle open HDF5 handles. If you wrap your own
> HDF5 dataset, set `multiprocessing.set_start_method("fork")` or open the
> file lazily inside each worker.

## Quickstart

```python
from pyunwrap.synthetic.generator import InSARSyntheticGenerator
from pyunwrap.models.ambiguity_net import AmbiguityNet
from pyunwrap.inference.unwrapper import PhaseUnwrapper

# Generate a synthetic sample (Mogi volcanic source deformation).
gen = InSARSyntheticGenerator(size=256, seed=42)
sample = gen.generate_sample(deformation_type="mogi")

# Tiled inference on an interferogram with a trained model.
model = AmbiguityNet(pretrained=False, k_max=10.0)  # or load your own checkpoint
unwrapper = PhaseUnwrapper(model=model, device="cuda")
result = unwrapper.unwrap(
    wrapped_phase_path="data/wrapped_phase.tif",
    coherence_path="data/coherence.tif",
    amplitude_path="data/amplitude.tif",
    tile_size=512, overlap=64,
    generate_report=True,
)
result.save_geotiff("unwrapped_output.tif")
```

Training a model end to end:

```bash
pyunwrap-train \
  --train-hdf5 train_tiles.h5 --val-hdf5 val_tiles.h5 \
  --epochs 60 --warmup-epochs 5 \
  --finetune-hdf5 snaphu_pseudo_gt.h5 --finetune-start-epoch 55 \
  --use-curriculum-replay --smoothness-weight 0.1 \
  --out-dir runs/pyunwrap_v1
```

Or from a real (or real-like) data stack, via the
[real-data injection pipeline](#key-features):

```bash
pyunwrap-train --real-data-path ./my_real_stack/ --epochs 60 --out-dir runs/pyunwrap_v1
```

Inference from the command line:

```bash
pyunwrap-unwrap \
  --wrapped-phase wrapped.tif --coherence coherence.tif --amplitude amplitude.tif \
  --checkpoint model.pt --output unwrapped.tif
  # add --no-hybrid to use the learned model alone
```

## Notebooks

The notebooks in [`notebooks/`](notebooks/) walk through the package
hands-on. The first three are checked in **pre-executed with real outputs**:

- [`01_training_pipeline.ipynb`](notebooks/01_training_pipeline.ipynb): the
  training chain: synthetic data, tiling, `AmbiguityNet` + `Trainer`, training
  curves, predicted-vs-truth comparison.
- [`02_full_pipeline.ipynb`](notebooks/02_full_pipeline.ipynb): the
  end-to-end chain: deformation-model gallery, tiling and augmentation,
  curriculum training with every loss component plotted, tiled inference on
  GeoTIFFs, residue/Nyquist/error analytics, Grad-CAM and Integrated
  Gradients, 3D and interactive-map visualization, ONNX deployment with a
  PyTorch-vs-ONNX agreement check, and the HTML report.
- [`03_training_bug_and_fix.ipynb`](notebooks/03_training_bug_and_fix.ipynb):
  a debugging record: a real LR-schedule bug found on a completed 60-epoch
  run, the fix, and a before/after benchmark. It also reports a second
  finding that does not cleanly replicate at a different scale.
- [`08_advanced_gpu_pipeline.ipynb`](notebooks/08_advanced_gpu_pipeline.ipynb):
  the GPU pipeline with the staged curriculum, fringe-sized scenes, an
  offset-invariant loss (with a numerical check), the skill metric with
  do-nothing and ceiling baselines, an easy-only diagnostic switch, tiled
  inference, analytics and the HTML report. It ships without outputs; run it
  on a GPU (about 16 minutes for 60 epochs on an RTX 3060 Laptop, 0.96 GB peak
  memory at the default batch size).

> [!NOTE]
> Notebooks 01-03 were written before the do-nothing baseline was added. Their
> benchmark comparisons against SNAPHU should be read with the caveat in
> [How we judge a model honestly](#how-we-judge-a-model-honestly).

```bash
pip install -e ".[dev,maps,notebooks]"
jupyter notebook notebooks/
```

## Documentation

The full documentation site (installation, quickstart, architecture guide,
tutorial notebooks rendered with their outputs, and the API reference
generated from source) is built with Sphinx and hosted at
**[pyunwrap.readthedocs.io](https://pyunwrap.readthedocs.io)**.

> [!NOTE]
> The Read the Docs badge/link will show as unbuilt until the project is
> activated at readthedocs.org under whichever account hosts this repo; the
> `.readthedocs.yaml` config and `docs/` source are ready.

Build it locally:

```bash
pip install -e ".[docs]"
sphinx-build -b html docs docs/_build/html
```

## Testing

```bash
pytest -m "not slow"              # fast unit + integration tests
pytest                             # full suite, including the end-to-end
                                    # synthetic → train → infer → report test
pytest --cov=pyunwrap --cov-report=term-missing
```

The suite includes known-answer physics tests (e.g. residue detection is
checked against a hand-constructed phase vortex with an exact, known
topological charge) and a tile-merging regression test asserting that
tiled-and-merged inference is *numerically identical* to a whole-image pass.
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the breakdown and setup.


## Project status

Early-stage and actively developed. The goal is **a model that demonstrably
beats a do-nothing baseline and is honest about where it beats classical
unwrapping**, not a blanket claim to beat SNAPHU. The pipeline (synthetic
data, real-data injection, curriculum training, tiled inference, the hybrid
router, analytics and reporting) is implemented and tested end to end.

**Where the learned model stands (latest internal run, synthetic validation
data, not a released checkpoint).** With the staged curriculum above, a
60-epoch run on a single laptop GPU reached, on held-out synthetic scenes:

| | Easy | Moderate | Hard |
|---|---|---|---|
| Skill vs do-nothing | ≈ +55 % | ≈ +50 % | ≈ 0 % |
| Best achievable (oracle ceiling) | ≈ 100 % | ≈ 98 % | ≈ 66 % |

and about 77 % of pixels with exactly the right cycle count, against 56 % for
do-nothing. In words: the model has learned something real on gentle scenes
and about half of what is possible there; it has **not** yet learned hard
scenes, which is the regime the hybrid design most needs. Progress stopped
improving after roughly epoch 15 in that run; the next experiments are
documented in the advanced notebook.

Worth knowing before relying on this:

- **No pretrained weights ship yet.** `from_pretrained()` downloads from a
  Zenodo record you supply; the example notebooks train demo models from
  scratch.
- **No validation against real satellite (e.g. Sentinel-1) data yet.** Every
  result is against `pyunwrap`'s own synthetic generator, or a
  clearly-labelled real-*like* fixture for `pyunwrap.data.real_injection`.
- **The real-data injection pipeline has not been run on genuinely real
  data.** Validating it on real Sentinel-1 stacks is unstarted future work.
- **Earlier low-coherence benchmark claims are withdrawn** (see above) until
  re-run with a do-nothing baseline.

APIs may change between minor versions until `1.0`. See
[`CHANGELOG.md`](CHANGELOG.md).

## Relationship to the wider EO stack

`pyunwrap` works standalone, but is designed to sit downstream of
[`pygeofetch`](#) (interferogram acquisition/formation) and alongside
[`ps-gnn`](#) (persistent scatterer identification) in an open-source InSAR
processing stack.

## Citation

If `pyunwrap` is useful in your research, please cite the archived release via
its Zenodo DOI:

```bibtex
@software{pyunwrap2026,
  title  = {pyunwrap: Physics-Informed Deep Learning for InSAR Phase Unwrapping},
  author = {Appiah Kubi, Samuel},
  year   = {2026},
  url    = {https://github.com/EOCoreINT/pyunwrap},
  doi    = {10.5281/zenodo.22209219},
  note   = {Version 0.1.0}
}
```

## References

- Goldstein, R. M., Zebker, H. A., & Werner, C. L. (1988). Satellite radar
  interferometry: Two-dimensional phase unwrapping. *Radio Science*, 23(4).
- Itoh, K. (1982). Analysis of the phase unwrapping algorithm. *Applied
  Optics*, 21(14).
- Chen, C. W., & Zebker, H. A. (2001). Two-dimensional phase unwrapping with
  use of statistical models for cost functions in nonlinear optimization
  (SNAPHU). *JOSA A*, 18(2).
- Okada, Y. (1985). Surface deformation due to shear and tensile faults in a
  half-space. *Bulletin of the Seismological Society of America*, 75(4).
- Mogi, K. (1958). Relations between the eruptions of various volcanoes and
  the deformations of the ground surfaces around them. *Bulletin of the
  Earthquake Research Institute*, 36.

## Contributing

Contributions are welcome: bug reports, documentation, new deformation
models, or core improvements. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for
development setup, test/lint conventions, and the design principles worth
knowing before touching the physics-critical modules.

## License

[MIT](LICENSE)
