pyunwrap-insar 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 (38) hide show
  1. pyunwrap_insar-0.1.0/LICENSE +21 -0
  2. pyunwrap_insar-0.1.0/PKG-INFO +358 -0
  3. pyunwrap_insar-0.1.0/README.md +305 -0
  4. pyunwrap_insar-0.1.0/pyproject.toml +116 -0
  5. pyunwrap_insar-0.1.0/pyunwrap/__init__.py +0 -0
  6. pyunwrap_insar-0.1.0/pyunwrap/analytics/__init__.py +0 -0
  7. pyunwrap_insar-0.1.0/pyunwrap/analytics/explainability.py +331 -0
  8. pyunwrap_insar-0.1.0/pyunwrap/analytics/phase_stats.py +374 -0
  9. pyunwrap_insar-0.1.0/pyunwrap/analytics/report_generator.py +404 -0
  10. pyunwrap_insar-0.1.0/pyunwrap/data/__init__.py +0 -0
  11. pyunwrap_insar-0.1.0/pyunwrap/data/dataloader.py +243 -0
  12. pyunwrap_insar-0.1.0/pyunwrap/data/preprocessing.py +417 -0
  13. pyunwrap_insar-0.1.0/pyunwrap/inference/__init__.py +0 -0
  14. pyunwrap_insar-0.1.0/pyunwrap/inference/unwrapper.py +650 -0
  15. pyunwrap_insar-0.1.0/pyunwrap/models/__init__.py +0 -0
  16. pyunwrap_insar-0.1.0/pyunwrap/models/ambiguity_net.py +367 -0
  17. pyunwrap_insar-0.1.0/pyunwrap/models/losses.py +257 -0
  18. pyunwrap_insar-0.1.0/pyunwrap/synthetic/__init__.py +0 -0
  19. pyunwrap_insar-0.1.0/pyunwrap/synthetic/generator.py +992 -0
  20. pyunwrap_insar-0.1.0/pyunwrap/training/__init__.py +0 -0
  21. pyunwrap_insar-0.1.0/pyunwrap/training/trainer.py +754 -0
  22. pyunwrap_insar-0.1.0/pyunwrap/utils/__init__.py +0 -0
  23. pyunwrap_insar-0.1.0/pyunwrap/utils/deployment.py +368 -0
  24. pyunwrap_insar-0.1.0/pyunwrap/visualization/__init__.py +0 -0
  25. pyunwrap_insar-0.1.0/pyunwrap/visualization/charts.py +179 -0
  26. pyunwrap_insar-0.1.0/pyunwrap/visualization/maps.py +223 -0
  27. pyunwrap_insar-0.1.0/pyunwrap/visualization/phase_plots.py +195 -0
  28. pyunwrap_insar-0.1.0/pyunwrap_insar.egg-info/PKG-INFO +358 -0
  29. pyunwrap_insar-0.1.0/pyunwrap_insar.egg-info/SOURCES.txt +36 -0
  30. pyunwrap_insar-0.1.0/pyunwrap_insar.egg-info/dependency_links.txt +1 -0
  31. pyunwrap_insar-0.1.0/pyunwrap_insar.egg-info/entry_points.txt +2 -0
  32. pyunwrap_insar-0.1.0/pyunwrap_insar.egg-info/requires.txt +33 -0
  33. pyunwrap_insar-0.1.0/pyunwrap_insar.egg-info/top_level.txt +1 -0
  34. pyunwrap_insar-0.1.0/setup.cfg +4 -0
  35. pyunwrap_insar-0.1.0/tests/test_model.py +211 -0
  36. pyunwrap_insar-0.1.0/tests/test_physics.py +243 -0
  37. pyunwrap_insar-0.1.0/tests/test_pipeline.py +303 -0
  38. pyunwrap_insar-0.1.0/tests/test_synthetic.py +271 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pyunwrap 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.
@@ -0,0 +1,358 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyunwrap-insar
3
+ Version: 0.1.0
4
+ 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.
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourusername/pyunwrap
8
+ Project-URL: Repository, https://github.com/yourusername/pyunwrap
9
+ Project-URL: Issues, https://github.com/yourusername/pyunwrap/issues
10
+ Keywords: InSAR,phase-unwrapping,SAR,remote-sensing,deep-learning,earth-observation,geospatial
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: GIS
14
+ Classifier: Topic :: Scientific/Engineering :: Physics
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.1
23
+ Requires-Dist: rasterio>=1.3
24
+ Requires-Dist: geopandas>=0.14
25
+ Requires-Dist: numpy>=1.24
26
+ Requires-Dist: scikit-image>=0.22
27
+ Requires-Dist: scikit-learn>=1.3
28
+ Requires-Dist: shap>=0.44
29
+ Requires-Dist: plotly>=5.18
30
+ Requires-Dist: pyvista>=0.43
31
+ Requires-Dist: jinja2>=3.1
32
+ Requires-Dist: onnxruntime>=1.16
33
+ Requires-Dist: scipy>=1.11
34
+ Requires-Dist: tqdm>=4.66
35
+ Requires-Dist: pandas>=2.1
36
+ Requires-Dist: tensorboard>=2.15
37
+ Requires-Dist: h5py>=3.10
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest>=7.4; extra == "dev"
40
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
41
+ Requires-Dist: black>=23.12; extra == "dev"
42
+ Requires-Dist: ruff>=0.1.9; extra == "dev"
43
+ Requires-Dist: mypy>=1.7; extra == "dev"
44
+ Provides-Extra: maps
45
+ Requires-Dist: folium>=0.15; extra == "maps"
46
+ Requires-Dist: leafmap>=0.30; extra == "maps"
47
+ Provides-Extra: deploy
48
+ Requires-Dist: openvino>=2023.2; extra == "deploy"
49
+ Requires-Dist: onnx>=1.16; extra == "deploy"
50
+ Requires-Dist: onnxconverter-common>=1.14; extra == "deploy"
51
+ Requires-Dist: onnxscript>=0.1; extra == "deploy"
52
+ Dynamic: license-file
53
+
54
+ <div align="center">
55
+
56
+ <picture>
57
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-horizontal-dark.svg">
58
+ <img src="assets/logo-horizontal.svg" alt="pyunwrap" width="420">
59
+ </picture>
60
+
61
+ ## ⚠️ Important Note
62
+
63
+ This project is **actively under development**. While the core functionality
64
+ is production-ready and thoroughly tested, some advanced features are still
65
+ being refined.
66
+
67
+ **Physics-informed deep learning for InSAR phase unwrapping.**
68
+
69
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
70
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](pyproject.toml)
71
+ [![Tests: 65 passing](https://img.shields.io/badge/tests-65%20passing-brightgreen)](tests/)
72
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
73
+ [![Status: early-stage](https://img.shields.io/badge/status-early--stage-orange)](#project-status)
74
+
75
+ [Why this exists](#why-this-exists) ·
76
+ [How it works](#how-it-works) ·
77
+ [Install](#installation) ·
78
+ [Quickstart](#quickstart) ·
79
+ [Notebooks](#notebooks) ·
80
+ [Architecture](#architecture) ·
81
+ [Citation](#citation)
82
+
83
+ </div>
84
+
85
+ ---
86
+
87
+ `pyunwrap` unwraps Interferometric Synthetic Aperture Radar (InSAR) phase — the
88
+ core measurement behind satellite-based ground-deformation monitoring — using a
89
+ **physics-informed U-Net that predicts an integer ambiguity map**, not the
90
+ unwrapped phase itself. The rewrapping identity is enforced by construction,
91
+ so the model is structurally incapable of producing an output that
92
+ contradicts the observed wrapped phase; it can only be wrong about *how many*
93
+ `2π` cycles were missed, never about the physics of wrapping.
94
+
95
+ ## Why this exists
96
+
97
+ Every classical phase-unwrapping algorithm — branch-cut methods, minimum-cost
98
+ flow (the approach behind SNAPHU, the field's long-standing reference tool),
99
+ Goldstein's algorithm — rests on one assumption: that the true phase gradient
100
+ between adjacent pixels never exceeds `π` radians (the Nyquist/Itoh
101
+ condition). Two situations break that assumption in practice:
102
+
103
+ - **Low coherence.** Vegetation, water, and temporal decorrelation add
104
+ near-random phase noise. Once the local gradient becomes unreliable,
105
+ branch-cut and MCF methods don't just get that one pixel wrong — they
106
+ propagate the error across the connected region downstream of it.
107
+ - **Steep deformation gradients.** Earthquakes, volcanic inflation, and
108
+ mining subsidence can produce genuine phase gradients that exceed `π`
109
+ radians per pixel near the source. No amount of algorithmic cleverness
110
+ recovers this from the wrapped phase alone without an external prior —
111
+ which is exactly the gap a learned model can fill.
112
+
113
+ `pyunwrap` targets both regimes directly, and does so without inheriting the
114
+ most common failure mode of naive deep-learning approaches to this problem:
115
+ regressing the unwrapped phase directly gives a network no reason to respect
116
+ `wrap(prediction) == observed_phase`. Predicting the integer ambiguity
117
+ instead makes that identity a mathematical guarantee, not a hope.
118
+
119
+ ## How it works
120
+
121
+ `AmbiguityNet` outputs a continuous ambiguity prediction, rounds it via a
122
+ straight-through estimator, and reconstructs phase directly:
123
+
124
+ ```
125
+ φ̂ = ψ + 2π · round(k̂), k̂ ∈ ℤ
126
+ ```
127
+
128
+ | Approach | Failure mode |
129
+ |---|---|
130
+ | Regress `φ` directly | Nothing constrains `wrap(prediction) == ψ`; the network can output any real value |
131
+ | **Predict `k` (this package)** | `wrap(ψ + 2π·round(k̂)) == ψ` holds by construction — the network only has to get the *integer cycle count* right |
132
+
133
+ That single design decision shapes everything downstream: training uses a
134
+ physics-informed loss with a component that's provably near-zero regardless
135
+ of prediction quality (documented explicitly, not hidden — see
136
+ [`pyunwrap/models/losses.py`](pyunwrap/models/losses.py)); tiled inference
137
+ merges the **integer ambiguity map** across overlapping patches, never the
138
+ phase itself, because averaging phase directly across a tile boundary can
139
+ silently produce a value that satisfies no physical interferogram.
140
+
141
+ ## Key features
142
+
143
+ **Synthetic data engine** ([`pyunwrap.synthetic`](pyunwrap/synthetic/generator.py))
144
+ — Gaussian subsidence bowls, a from-scratch Okada (1985) rectangular fault
145
+ dislocation model, a Mogi (1958) volcanic point source, DEM-driven
146
+ topographic phase, Kolmogorov-spectrum atmospheric turbulence, orbital
147
+ ramps, coherence-dependent decorrelation noise, and a pseudo-real strategy
148
+ that rewraps real L-band (ALOS-2) unwrapped phase into simulated C-band data
149
+ to help bridge the sim-to-real gap.
150
+
151
+ **`AmbiguityNet`** ([`pyunwrap.models`](pyunwrap/models/ambiguity_net.py))
152
+ — a ResNet-34-encoder U-Net with a dual head: the integer ambiguity map via
153
+ a straight-through-estimator rounding layer, and an auxiliary residue-
154
+ probability map for uncertainty. Trained with a four-component
155
+ physics-informed loss (ambiguity regression, re-wrap consistency,
156
+ coherence-weighted smoothness, ambiguity-map residue penalty).
157
+
158
+ **Curriculum training** ([`pyunwrap.training`](pyunwrap/training/trainer.py))
159
+ — a three-stage curriculum (high-coherence/low-gradient → moderate →
160
+ full difficulty), optional SNAPHU pseudo-ground-truth fine-tuning on real
161
+ data, AdamW with warmup/cosine annealing, and TensorBoard logging.
162
+
163
+ **Production inference** ([`pyunwrap.inference`](pyunwrap/inference/unwrapper.py))
164
+ — tiled processing of arbitrarily large interferograms with edge-aware,
165
+ residue-probability-weighted smart merging of the ambiguity map, Monte
166
+ Carlo Dropout uncertainty, and an ONNX Runtime → OpenVINO backend fallback
167
+ chain for deployment without a PyTorch dependency.
168
+
169
+ **Scientific analytics** ([`pyunwrap.analytics`](pyunwrap/analytics/))
170
+ — Goldstein-style residue detection and clustering, Nyquist
171
+ gradient-violation mapping, error-distribution statistics, Grad-CAM and
172
+ Integrated-Gradients explainability, and uncertainty-calibration
173
+ reliability diagrams.
174
+
175
+ **Visualization & reporting** ([`pyunwrap.visualization`](pyunwrap/visualization/))
176
+ — interactive Plotly 3D phase surfaces and heatmaps, a folium
177
+ swipe-comparison map, and a self-contained, Jinja2-templated HTML report
178
+ tying every stage together.
179
+
180
+ ## Architecture
181
+
182
+ ```
183
+ InSARSyntheticGenerator ──┐ real GeoTIFFs
184
+ │ (wrapped, coherence, amplitude)
185
+ ▼ │
186
+ tiling + normalization ▼
187
+ │ tiled inference
188
+ ▼ via PhaseUnwrapper
189
+ InSARTileDataset │
190
+ │ │
191
+ ▼ ▼
192
+ Trainer.fit() ──── AmbiguityNet ──── smart ambiguity-map
193
+ (curriculum, (this is the merging (never
194
+ physics loss) same model) the phase)
195
+ │
196
+ ▼
197
+ unwrapped phase +
198
+ analytics + report
199
+ ```
200
+
201
+ See [`docs/architecture.md`](docs/architecture.md) for the full
202
+ module-by-module reference and a longer explanation of the physics
203
+ invariant every stage is built around.
204
+
205
+ ## Installation
206
+
207
+ ```bash
208
+ git clone https://github.com/yourusername/pyunwrap.git
209
+ cd pyunwrap
210
+ pip install -e .
211
+ ```
212
+
213
+ Optional extras, installed as needed:
214
+
215
+ | Extra | Adds | Use case |
216
+ |---|---|---|
217
+ | `dev` | `pytest`, `pytest-cov`, `black`, `ruff`, `mypy` | Development, testing, linting |
218
+ | `maps` | `folium`, `leafmap` | Interactive map visualization |
219
+ | `deploy` | `openvino`, `onnx`, `onnxconverter-common`, `onnxscript` | ONNX export, OpenVINO inference |
220
+ | `notebooks` | `jupyter`, `ipykernel` | Running the example notebooks |
221
+
222
+ ```bash
223
+ pip install -e ".[dev,maps,deploy,notebooks]" # everything
224
+ ```
225
+
226
+ ## Quickstart
227
+
228
+ ```python
229
+ from pyunwrap.synthetic.generator import InSARSyntheticGenerator
230
+ from pyunwrap.models.ambiguity_net import AmbiguityNet
231
+ from pyunwrap.inference.unwrapper import PhaseUnwrapper
232
+
233
+ # Generate a synthetic training sample (Mogi volcanic source deformation).
234
+ gen = InSARSyntheticGenerator(size=256, seed=42)
235
+ sample = gen.generate_sample(deformation_type="mogi")
236
+
237
+ # Run tiled inference on a real interferogram with a trained model.
238
+ model = AmbiguityNet(pretrained=False, k_max=10.0) # or load your own checkpoint
239
+ unwrapper = PhaseUnwrapper(model=model, device="cuda")
240
+ result = unwrapper.unwrap(
241
+ wrapped_phase_path="data/wrapped_phase.tif",
242
+ coherence_path="data/coherence.tif",
243
+ amplitude_path="data/amplitude.tif",
244
+ tile_size=512, overlap=64,
245
+ generate_report=True,
246
+ )
247
+ result.save_geotiff("unwrapped_output.tif")
248
+ ```
249
+
250
+ Training a model end to end:
251
+
252
+ ```bash
253
+ pyunwrap-train \
254
+ --train-hdf5 train_tiles.h5 --val-hdf5 val_tiles.h5 \
255
+ --epochs 60 --warmup-epochs 5 \
256
+ --finetune-hdf5 snaphu_pseudo_gt.h5 --finetune-start-epoch 55 \
257
+ --out-dir runs/pyunwrap_v1
258
+ ```
259
+
260
+ ## Notebooks
261
+
262
+ Two notebooks in [`notebooks/`](notebooks/) walk through the package
263
+ hands-on, checked in **pre-executed with real outputs** so they're readable
264
+ without running anything:
265
+
266
+ - [`01_training_pipeline.ipynb`](notebooks/01_training_pipeline.ipynb) —
267
+ the complete training chain: synthetic data, tiling, `AmbiguityNet` +
268
+ `Trainer`, training curves, and predicted-vs-ground-truth comparison.
269
+ - [`02_full_pipeline.ipynb`](notebooks/02_full_pipeline.ipynb) — the
270
+ complete end-to-end chain: a deformation-model gallery, tiling and
271
+ augmentation visualized, a full curriculum training run with every loss
272
+ component plotted, tiled inference on real GeoTIFFs, residue/Nyquist/error
273
+ analytics, Grad-CAM and Integrated-Gradients explainability, 3D and
274
+ interactive-map visualization, ONNX deployment with a numerical
275
+ PyTorch-vs-ONNX agreement check, and the final HTML report.
276
+
277
+ ```bash
278
+ pip install -e ".[dev,maps,notebooks]"
279
+ jupyter notebook notebooks/
280
+ ```
281
+
282
+ ## Testing
283
+
284
+ ```bash
285
+ pytest -m "not slow" # fast unit + integration tests
286
+ pytest # full suite, including the end-to-end
287
+ # synthetic → train → infer → report test
288
+ pytest --cov=pyunwrap --cov-report=term-missing
289
+ ```
290
+
291
+ The suite includes known-answer physics tests (e.g. residue detection is
292
+ checked against a hand-constructed phase vortex with an exact, known
293
+ topological charge — not just "runs without crashing") and a tile-merging
294
+ regression test that asserts tiled-and-merged inference is *numerically
295
+ identical* to a whole-image pass, directly targeting the class of bug where
296
+ tile boundaries silently corrupt the output. See
297
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full breakdown and development
298
+ setup.
299
+
300
+
301
+ ## Project status
302
+
303
+ Early-stage and actively developed. The pipeline — synthetic data
304
+ generation, training, tiled inference, analytics, and reporting — is
305
+ implemented and tested end to end, but **no pretrained weights ship yet**;
306
+ `from_pretrained()` downloads from a Zenodo record you supply, and the
307
+ example notebooks train small demo models from scratch rather than loading
308
+ a benchmarked checkpoint. Monte Carlo Dropout uncertainty is currently a
309
+ no-op (`AmbiguityNet` has no `nn.Dropout` layers yet, only BatchNorm) —
310
+ tracked as a known gap, not hidden. APIs may change between minor versions
311
+ until `1.0`. See [`CHANGELOG.md`](CHANGELOG.md) for what's landed so far.
312
+
313
+ ## Relationship to the wider EO stack
314
+
315
+ `pyunwrap` works standalone, but is designed to eventually sit downstream of
316
+ [`pygeofetch`](#) (interferogram acquisition/formation) and alongside
317
+ [`ps-gnn`](#) (persistent scatterer identification) in a broader open-source
318
+ InSAR processing stack.
319
+
320
+ ## Citation
321
+
322
+ If `pyunwrap` is useful in your research, please cite it:
323
+
324
+ ```bibtex
325
+ @software{pyunwrap2026,
326
+ title = {pyunwrap: Physics-Informed Deep Learning for InSAR Phase Unwrapping},
327
+ author = {{pyunwrap contributors}},
328
+ year = {2026},
329
+ url = {https://github.com/yourusername/pyunwrap},
330
+ note = {Version 0.1.0}
331
+ }
332
+ ```
333
+
334
+ ## References
335
+
336
+ - Goldstein, R. M., Zebker, H. A., & Werner, C. L. (1988). Satellite radar
337
+ interferometry: Two-dimensional phase unwrapping. *Radio Science*, 23(4).
338
+ - Itoh, K. (1982). Analysis of the phase unwrapping algorithm. *Applied
339
+ Optics*, 21(14).
340
+ - Chen, C. W., & Zebker, H. A. (2001). Two-dimensional phase unwrapping with
341
+ use of statistical models for cost functions in nonlinear optimization
342
+ (SNAPHU). *JOSA A*, 18(2).
343
+ - Okada, Y. (1985). Surface deformation due to shear and tensile faults in a
344
+ half-space. *Bulletin of the Seismological Society of America*, 75(4).
345
+ - Mogi, K. (1958). Relations between the eruptions of various volcanoes and
346
+ the deformations of the ground surfaces around them. *Bulletin of the
347
+ Earthquake Research Institute*, 36.
348
+
349
+ ## Contributing
350
+
351
+ Contributions are welcome — bug reports, documentation, new deformation
352
+ models, or core improvements. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for
353
+ development setup, test/lint conventions, and the design principles worth
354
+ knowing before touching the physics-critical modules.
355
+
356
+ ## License
357
+
358
+ [MIT](LICENSE)
@@ -0,0 +1,305 @@
1
+ <div align="center">
2
+
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-horizontal-dark.svg">
5
+ <img src="assets/logo-horizontal.svg" alt="pyunwrap" 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
+ **Physics-informed deep learning for InSAR phase unwrapping.**
15
+
16
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
17
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](pyproject.toml)
18
+ [![Tests: 65 passing](https://img.shields.io/badge/tests-65%20passing-brightgreen)](tests/)
19
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
20
+ [![Status: early-stage](https://img.shields.io/badge/status-early--stage-orange)](#project-status)
21
+
22
+ [Why this exists](#why-this-exists) ·
23
+ [How it works](#how-it-works) ·
24
+ [Install](#installation) ·
25
+ [Quickstart](#quickstart) ·
26
+ [Notebooks](#notebooks) ·
27
+ [Architecture](#architecture) ·
28
+ [Citation](#citation)
29
+
30
+ </div>
31
+
32
+ ---
33
+
34
+ `pyunwrap` unwraps Interferometric Synthetic Aperture Radar (InSAR) phase — the
35
+ core measurement behind satellite-based ground-deformation monitoring — using a
36
+ **physics-informed U-Net that predicts an integer ambiguity map**, not the
37
+ unwrapped phase itself. The rewrapping identity is enforced by construction,
38
+ so the model is structurally incapable of producing an output that
39
+ contradicts the observed wrapped phase; it can only be wrong about *how many*
40
+ `2π` cycles were missed, never about the physics of wrapping.
41
+
42
+ ## Why this exists
43
+
44
+ Every classical phase-unwrapping algorithm — branch-cut methods, minimum-cost
45
+ flow (the approach behind SNAPHU, the field's long-standing reference tool),
46
+ Goldstein's algorithm — rests on one assumption: that the true phase gradient
47
+ between adjacent pixels never exceeds `π` radians (the Nyquist/Itoh
48
+ condition). Two situations break that assumption in practice:
49
+
50
+ - **Low coherence.** Vegetation, water, and temporal decorrelation add
51
+ near-random phase noise. Once the local gradient becomes unreliable,
52
+ branch-cut and MCF methods don't just get that one pixel wrong — they
53
+ propagate the error across the connected region downstream of it.
54
+ - **Steep deformation gradients.** Earthquakes, volcanic inflation, and
55
+ mining subsidence can produce genuine phase gradients that exceed `π`
56
+ radians per pixel near the source. No amount of algorithmic cleverness
57
+ recovers this from the wrapped phase alone without an external prior —
58
+ which is exactly the gap a learned model can fill.
59
+
60
+ `pyunwrap` targets both regimes directly, and does so without inheriting the
61
+ most common failure mode of naive deep-learning approaches to this problem:
62
+ regressing the unwrapped phase directly gives a network no reason to respect
63
+ `wrap(prediction) == observed_phase`. Predicting the integer ambiguity
64
+ instead makes that identity a mathematical guarantee, not a hope.
65
+
66
+ ## How it works
67
+
68
+ `AmbiguityNet` outputs a continuous ambiguity prediction, rounds it via a
69
+ straight-through estimator, and reconstructs phase directly:
70
+
71
+ ```
72
+ φ̂ = ψ + 2π · round(k̂), k̂ ∈ ℤ
73
+ ```
74
+
75
+ | Approach | Failure mode |
76
+ |---|---|
77
+ | Regress `φ` directly | Nothing constrains `wrap(prediction) == ψ`; the network can output any real value |
78
+ | **Predict `k` (this package)** | `wrap(ψ + 2π·round(k̂)) == ψ` holds by construction — the network only has to get the *integer cycle count* right |
79
+
80
+ That single design decision shapes everything downstream: training uses a
81
+ physics-informed loss with a component that's provably near-zero regardless
82
+ of prediction quality (documented explicitly, not hidden — see
83
+ [`pyunwrap/models/losses.py`](pyunwrap/models/losses.py)); tiled inference
84
+ merges the **integer ambiguity map** across overlapping patches, never the
85
+ phase itself, because averaging phase directly across a tile boundary can
86
+ silently produce a value that satisfies no physical interferogram.
87
+
88
+ ## Key features
89
+
90
+ **Synthetic data engine** ([`pyunwrap.synthetic`](pyunwrap/synthetic/generator.py))
91
+ — Gaussian subsidence bowls, a from-scratch Okada (1985) rectangular fault
92
+ dislocation model, a Mogi (1958) volcanic point source, DEM-driven
93
+ topographic phase, Kolmogorov-spectrum atmospheric turbulence, orbital
94
+ ramps, coherence-dependent decorrelation noise, and a pseudo-real strategy
95
+ that rewraps real L-band (ALOS-2) unwrapped phase into simulated C-band data
96
+ to help bridge the sim-to-real gap.
97
+
98
+ **`AmbiguityNet`** ([`pyunwrap.models`](pyunwrap/models/ambiguity_net.py))
99
+ — a ResNet-34-encoder U-Net with a dual head: the integer ambiguity map via
100
+ a straight-through-estimator rounding layer, and an auxiliary residue-
101
+ probability map for uncertainty. Trained with a four-component
102
+ physics-informed loss (ambiguity regression, re-wrap consistency,
103
+ coherence-weighted smoothness, ambiguity-map residue penalty).
104
+
105
+ **Curriculum training** ([`pyunwrap.training`](pyunwrap/training/trainer.py))
106
+ — a three-stage curriculum (high-coherence/low-gradient → moderate →
107
+ full difficulty), optional SNAPHU pseudo-ground-truth fine-tuning on real
108
+ data, AdamW with warmup/cosine annealing, and TensorBoard logging.
109
+
110
+ **Production inference** ([`pyunwrap.inference`](pyunwrap/inference/unwrapper.py))
111
+ — tiled processing of arbitrarily large interferograms with edge-aware,
112
+ residue-probability-weighted smart merging of the ambiguity map, Monte
113
+ Carlo Dropout uncertainty, and an ONNX Runtime → OpenVINO backend fallback
114
+ chain for deployment without a PyTorch dependency.
115
+
116
+ **Scientific analytics** ([`pyunwrap.analytics`](pyunwrap/analytics/))
117
+ — Goldstein-style residue detection and clustering, Nyquist
118
+ gradient-violation mapping, error-distribution statistics, Grad-CAM and
119
+ Integrated-Gradients explainability, and uncertainty-calibration
120
+ reliability diagrams.
121
+
122
+ **Visualization & reporting** ([`pyunwrap.visualization`](pyunwrap/visualization/))
123
+ — interactive Plotly 3D phase surfaces and heatmaps, a folium
124
+ swipe-comparison map, and a self-contained, Jinja2-templated HTML report
125
+ tying every stage together.
126
+
127
+ ## Architecture
128
+
129
+ ```
130
+ InSARSyntheticGenerator ──┐ real GeoTIFFs
131
+ │ (wrapped, coherence, amplitude)
132
+ ▼ │
133
+ tiling + normalization ▼
134
+ │ tiled inference
135
+ ▼ via PhaseUnwrapper
136
+ InSARTileDataset │
137
+ │ │
138
+ ▼ ▼
139
+ Trainer.fit() ──── AmbiguityNet ──── smart ambiguity-map
140
+ (curriculum, (this is the merging (never
141
+ physics loss) same model) the phase)
142
+ │
143
+ ▼
144
+ unwrapped phase +
145
+ analytics + report
146
+ ```
147
+
148
+ See [`docs/architecture.md`](docs/architecture.md) for the full
149
+ module-by-module reference and a longer explanation of the physics
150
+ invariant every stage is built around.
151
+
152
+ ## Installation
153
+
154
+ ```bash
155
+ git clone https://github.com/yourusername/pyunwrap.git
156
+ cd pyunwrap
157
+ pip install -e .
158
+ ```
159
+
160
+ Optional extras, installed as needed:
161
+
162
+ | Extra | Adds | Use case |
163
+ |---|---|---|
164
+ | `dev` | `pytest`, `pytest-cov`, `black`, `ruff`, `mypy` | Development, testing, linting |
165
+ | `maps` | `folium`, `leafmap` | Interactive map visualization |
166
+ | `deploy` | `openvino`, `onnx`, `onnxconverter-common`, `onnxscript` | ONNX export, OpenVINO inference |
167
+ | `notebooks` | `jupyter`, `ipykernel` | Running the example notebooks |
168
+
169
+ ```bash
170
+ pip install -e ".[dev,maps,deploy,notebooks]" # everything
171
+ ```
172
+
173
+ ## Quickstart
174
+
175
+ ```python
176
+ from pyunwrap.synthetic.generator import InSARSyntheticGenerator
177
+ from pyunwrap.models.ambiguity_net import AmbiguityNet
178
+ from pyunwrap.inference.unwrapper import PhaseUnwrapper
179
+
180
+ # Generate a synthetic training sample (Mogi volcanic source deformation).
181
+ gen = InSARSyntheticGenerator(size=256, seed=42)
182
+ sample = gen.generate_sample(deformation_type="mogi")
183
+
184
+ # Run tiled inference on a real interferogram with a trained model.
185
+ model = AmbiguityNet(pretrained=False, k_max=10.0) # or load your own checkpoint
186
+ unwrapper = PhaseUnwrapper(model=model, device="cuda")
187
+ result = unwrapper.unwrap(
188
+ wrapped_phase_path="data/wrapped_phase.tif",
189
+ coherence_path="data/coherence.tif",
190
+ amplitude_path="data/amplitude.tif",
191
+ tile_size=512, overlap=64,
192
+ generate_report=True,
193
+ )
194
+ result.save_geotiff("unwrapped_output.tif")
195
+ ```
196
+
197
+ Training a model end to end:
198
+
199
+ ```bash
200
+ pyunwrap-train \
201
+ --train-hdf5 train_tiles.h5 --val-hdf5 val_tiles.h5 \
202
+ --epochs 60 --warmup-epochs 5 \
203
+ --finetune-hdf5 snaphu_pseudo_gt.h5 --finetune-start-epoch 55 \
204
+ --out-dir runs/pyunwrap_v1
205
+ ```
206
+
207
+ ## Notebooks
208
+
209
+ Two notebooks in [`notebooks/`](notebooks/) walk through the package
210
+ hands-on, checked in **pre-executed with real outputs** so they're readable
211
+ without running anything:
212
+
213
+ - [`01_training_pipeline.ipynb`](notebooks/01_training_pipeline.ipynb) —
214
+ the complete training chain: synthetic data, tiling, `AmbiguityNet` +
215
+ `Trainer`, training curves, and predicted-vs-ground-truth comparison.
216
+ - [`02_full_pipeline.ipynb`](notebooks/02_full_pipeline.ipynb) — the
217
+ complete end-to-end chain: a deformation-model gallery, tiling and
218
+ augmentation visualized, a full curriculum training run with every loss
219
+ component plotted, tiled inference on real GeoTIFFs, residue/Nyquist/error
220
+ analytics, Grad-CAM and Integrated-Gradients explainability, 3D and
221
+ interactive-map visualization, ONNX deployment with a numerical
222
+ PyTorch-vs-ONNX agreement check, and the final HTML report.
223
+
224
+ ```bash
225
+ pip install -e ".[dev,maps,notebooks]"
226
+ jupyter notebook notebooks/
227
+ ```
228
+
229
+ ## Testing
230
+
231
+ ```bash
232
+ pytest -m "not slow" # fast unit + integration tests
233
+ pytest # full suite, including the end-to-end
234
+ # synthetic → train → infer → report test
235
+ pytest --cov=pyunwrap --cov-report=term-missing
236
+ ```
237
+
238
+ The suite includes known-answer physics tests (e.g. residue detection is
239
+ checked against a hand-constructed phase vortex with an exact, known
240
+ topological charge — not just "runs without crashing") and a tile-merging
241
+ regression test that asserts tiled-and-merged inference is *numerically
242
+ identical* to a whole-image pass, directly targeting the class of bug where
243
+ tile boundaries silently corrupt the output. See
244
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full breakdown and development
245
+ setup.
246
+
247
+
248
+ ## Project status
249
+
250
+ Early-stage and actively developed. The pipeline — synthetic data
251
+ generation, training, tiled inference, analytics, and reporting — is
252
+ implemented and tested end to end, but **no pretrained weights ship yet**;
253
+ `from_pretrained()` downloads from a Zenodo record you supply, and the
254
+ example notebooks train small demo models from scratch rather than loading
255
+ a benchmarked checkpoint. Monte Carlo Dropout uncertainty is currently a
256
+ no-op (`AmbiguityNet` has no `nn.Dropout` layers yet, only BatchNorm) —
257
+ tracked as a known gap, not hidden. APIs may change between minor versions
258
+ until `1.0`. See [`CHANGELOG.md`](CHANGELOG.md) for what's landed so far.
259
+
260
+ ## Relationship to the wider EO stack
261
+
262
+ `pyunwrap` works standalone, but is designed to eventually sit downstream of
263
+ [`pygeofetch`](#) (interferogram acquisition/formation) and alongside
264
+ [`ps-gnn`](#) (persistent scatterer identification) in a broader open-source
265
+ InSAR processing stack.
266
+
267
+ ## Citation
268
+
269
+ If `pyunwrap` is useful in your research, please cite it:
270
+
271
+ ```bibtex
272
+ @software{pyunwrap2026,
273
+ title = {pyunwrap: Physics-Informed Deep Learning for InSAR Phase Unwrapping},
274
+ author = {{pyunwrap contributors}},
275
+ year = {2026},
276
+ url = {https://github.com/yourusername/pyunwrap},
277
+ note = {Version 0.1.0}
278
+ }
279
+ ```
280
+
281
+ ## References
282
+
283
+ - Goldstein, R. M., Zebker, H. A., & Werner, C. L. (1988). Satellite radar
284
+ interferometry: Two-dimensional phase unwrapping. *Radio Science*, 23(4).
285
+ - Itoh, K. (1982). Analysis of the phase unwrapping algorithm. *Applied
286
+ Optics*, 21(14).
287
+ - Chen, C. W., & Zebker, H. A. (2001). Two-dimensional phase unwrapping with
288
+ use of statistical models for cost functions in nonlinear optimization
289
+ (SNAPHU). *JOSA A*, 18(2).
290
+ - Okada, Y. (1985). Surface deformation due to shear and tensile faults in a
291
+ half-space. *Bulletin of the Seismological Society of America*, 75(4).
292
+ - Mogi, K. (1958). Relations between the eruptions of various volcanoes and
293
+ the deformations of the ground surfaces around them. *Bulletin of the
294
+ Earthquake Research Institute*, 36.
295
+
296
+ ## Contributing
297
+
298
+ Contributions are welcome — bug reports, documentation, new deformation
299
+ models, or core improvements. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for
300
+ development setup, test/lint conventions, and the design principles worth
301
+ knowing before touching the physics-critical modules.
302
+
303
+ ## License
304
+
305
+ [MIT](LICENSE)