Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ repos:
args: [--preview]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.16
rev: v0.16.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix, --show-fixes]
types_or: [python, jupyter]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v2.1.0"
rev: "v2.3.0"
hooks:
- id: mypy
additional_dependencies:
Expand Down
88 changes: 72 additions & 16 deletions docs/example_compare_sources.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"id": "26faf28f-336d-4ced-9be8-4217ef2deea7",
"metadata": {},
"source": [
"# Cross-source comparison\n",
"\n",
Expand All @@ -20,24 +20,80 @@
},
{
"cell_type": "markdown",
"metadata": {},
"id": "76c2bc1a-3c6c-4c4b-afb9-e979957bfcfe",
"metadata": {},
"source": [
"## Load the longest-baseline timeseries TIFF from each run"
]
},
{
"cell_type": "code",
"metadata": {},
"id": "862df8b7-dddd-4dca-a7a8-6e94e9a1da0c",
"execution_count": null,
"id": "862df8b7-dddd-4dca-a7a8-6e94e9a1da0c",
"metadata": {},
"outputs": [],
"source": "import re\nfrom datetime import datetime\nfrom pathlib import Path\n\nimport numpy as np\nimport rasterio\nimport matplotlib.pyplot as plt\n\n# Each per-source example notebook writes its outputs to ./example_<source>\n# relative to whatever directory it was launched from. Run this notebook from\n# the same CWD as those three notebooks.\nRUNS_DIR = Path(\".\")\nSOURCES = {\n \"Sentinel-1 bursts\": RUNS_DIR / \"example_s1_burst\",\n \"OPERA CSLC\": RUNS_DIR / \"example_opera_cslc\",\n \"NISAR GSLC\": RUNS_DIR / \"example_nisar\",\n}\n\nPAIR_RE = re.compile(r\"^(\\d{8})_(\\d{8})\\.tif$\")\n\ndef longest_pair(ts_dir: Path) -> tuple[Path, datetime, datetime]:\n \"\"\"Return the `YYYYMMDD_YYYYMMDD.tif` with the widest baseline.\"\"\"\n best = None\n best_span = -1\n for p in ts_dir.glob(\"*.tif\"):\n m = PAIR_RE.match(p.name)\n if not m:\n continue\n d1 = datetime.strptime(m.group(1), \"%Y%m%d\")\n d2 = datetime.strptime(m.group(2), \"%Y%m%d\")\n span = (d2 - d1).days\n if span > best_span:\n best_span = span\n best = (p, d1, d2)\n assert best is not None, f\"no pair-baseline TIFFs in {ts_dir}\"\n return best\n\ndef load_raster(path: Path):\n with rasterio.open(path) as src:\n return {\n \"data\": src.read(1, masked=True),\n \"bounds\": src.bounds,\n \"unit\": (src.units[0] if src.units else \"?\") or \"?\",\n }\n\nrasters = {}\nfor label, root in SOURCES.items():\n ts_dir = root / \"dolphin\" / \"timeseries\"\n longest_path, d1, d2 = longest_pair(ts_dir)\n r = load_raster(longest_path)\n r.update(pair_name=longest_path.name, date_start=d1, date_end=d2, baseline_days=(d2 - d1).days)\n rasters[label] = r\n print(\n f\"{label:20s} {longest_path.name} unit={r['unit']} baseline={r['baseline_days']}d\"\n )"
"source": [
"import re\n",
"from datetime import datetime\n",
"from pathlib import Path\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import rasterio\n",
"\n",
"# Each per-source example notebook writes its outputs to ./example_<source>\n",
"# relative to whatever directory it was launched from. Run this notebook from\n",
"# the same CWD as those three notebooks.\n",
"RUNS_DIR = Path(\".\")\n",
"SOURCES = {\n",
" \"Sentinel-1 bursts\": RUNS_DIR / \"example_s1_burst\",\n",
" \"OPERA CSLC\": RUNS_DIR / \"example_opera_cslc\",\n",
" \"NISAR GSLC\": RUNS_DIR / \"example_nisar\",\n",
"}\n",
"\n",
"PAIR_RE = re.compile(r\"^(\\d{8})_(\\d{8})\\.tif$\")\n",
"\n",
"def longest_pair(ts_dir: Path) -> tuple[Path, datetime, datetime]:\n",
" \"\"\"Return the `YYYYMMDD_YYYYMMDD.tif` with the widest baseline.\"\"\"\n",
" best = None\n",
" best_span = -1\n",
" for p in ts_dir.glob(\"*.tif\"):\n",
" m = PAIR_RE.match(p.name)\n",
" if not m:\n",
" continue\n",
" d1 = datetime.strptime(m.group(1), \"%Y%m%d\")\n",
" d2 = datetime.strptime(m.group(2), \"%Y%m%d\")\n",
" span = (d2 - d1).days\n",
" if span > best_span:\n",
" best_span = span\n",
" best = (p, d1, d2)\n",
" assert best is not None, f\"no pair-baseline TIFFs in {ts_dir}\"\n",
" return best\n",
"\n",
"def load_raster(path: Path):\n",
" with rasterio.open(path) as src:\n",
" return {\n",
" \"data\": src.read(1, masked=True),\n",
" \"bounds\": src.bounds,\n",
" \"unit\": (src.units[0] if src.units else \"?\") or \"?\",\n",
" }\n",
"\n",
"rasters = {}\n",
"for label, root in SOURCES.items():\n",
" ts_dir = root / \"dolphin\" / \"timeseries\"\n",
" longest_path, d1, d2 = longest_pair(ts_dir)\n",
" r = load_raster(longest_path)\n",
" r.update(pair_name=longest_path.name, date_start=d1, date_end=d2, baseline_days=(d2 - d1).days)\n",
" rasters[label] = r\n",
" print(\n",
" f\"{label:20s} {longest_path.name} unit={r['unit']} baseline={r['baseline_days']}d\"\n",
" )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"id": "00c745c0-7ffc-4d93-ab40-dd5dd39d3153",
"metadata": {},
"source": [
"## Cumulative displacement (meters)\n",
"\n",
Expand All @@ -46,9 +102,9 @@
},
{
"cell_type": "code",
"metadata": {},
"id": "96cbde80-7969-4ce8-89e2-5cdbf1eb1fa4",
"execution_count": null,
"id": "96cbde80-7969-4ce8-89e2-5cdbf1eb1fa4",
"metadata": {},
"outputs": [],
"source": [
"def finite_values(a):\n",
Expand Down Expand Up @@ -93,8 +149,8 @@
},
{
"cell_type": "markdown",
"metadata": {},
"id": "c9f60e41-bfc0-43f8-9c86-09e33e07440c",
"metadata": {},
"source": [
"## Effective velocity (meters / year)\n",
"\n",
Expand All @@ -103,9 +159,9 @@
},
{
"cell_type": "code",
"metadata": {},
"id": "80a7e0ee-35c5-486a-a580-c121c6871fe3",
"execution_count": null,
"id": "80a7e0ee-35c5-486a-a580-c121c6871fe3",
"metadata": {},
"outputs": [],
"source": [
"rasters_eff = {}\n",
Expand Down Expand Up @@ -143,17 +199,17 @@
},
{
"cell_type": "markdown",
"metadata": {},
"id": "eef5bc98-b369-4230-b9d8-295889f11efc",
"metadata": {},
"source": [
"## Quick stats"
]
},
{
"cell_type": "code",
"metadata": {},
"id": "4245053f-b51a-4d04-b741-745fb2bb04af",
"execution_count": null,
"id": "4245053f-b51a-4d04-b741-745fb2bb04af",
"metadata": {},
"outputs": [],
"source": [
"print(f\"{'source':20s} {'baseline':>10s} {'n_valid':>10s} {'disp_std':>12s} {'eff_v_std':>12s}\")\n",
Expand All @@ -178,9 +234,9 @@
"name": "python3"
},
"language_info": {
"name": "python",
"file_extension": ".py",
"mimetype": "text/x-python",
"file_extension": ".py"
"name": "python"
}
},
"nbformat": 4,
Expand Down
59 changes: 30 additions & 29 deletions docs/example_nisar.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"id": "85564ba9-fef1-4e33-824c-69a58469aff1",
"metadata": {},
"source": [
"# sweets example 3 \u2014 NISAR GSLC workflow\n",
"# sweets example 3 NISAR GSLC workflow\n",
"\n",
"End-to-end InSAR run over the same LA AOI, this time using pre-geocoded [NISAR L2 GSLC HDF5s](https://nisar.jpl.nasa.gov/) pulled from CMR. NISAR ships UTM-projected complex imagery at L-band, so the pipeline is the simplest of the three sources: no COMPASS, no burst database, no orbit files, no geometry stitching. sweets writes a ~1 KB VRT wrapper per polarization that injects the correct geotransform on top of the HDF5 subdataset (GDAL's HDF5 driver can't parse NISAR's separate xCoordinates/yCoordinates arrays), and hands the VRTs directly to dolphin.\n",
"\n",
"**Why use it?** NISAR is L-band (~24 cm wavelength) \u2014 cuts through vegetation and decorrelates much more slowly than Sentinel-1 C-band, so coherent stacks can span months instead of weeks.\n",
"**Why use it?** NISAR is L-band (~24 cm wavelength) cuts through vegetation and decorrelates much more slowly than Sentinel-1 C-band, so coherent stacks can span months instead of weeks.\n",
"\n",
"**AOI / time**: same LA polygon + 2025-12-01 -> 2025-12-31 window as the Sentinel-1 and OPERA example notebooks. NISAR track 34 frame 18 (the ASF Vertex fields `Track` and `Frame`) covers LA on an ascending pass during this period.\n",
"\n",
Expand All @@ -18,8 +18,8 @@
},
{
"cell_type": "markdown",
"metadata": {},
"id": "cbc5042b-98dd-455a-9523-f3d24cad15a1",
"metadata": {},
"source": [
"## Earthdata credentials\n",
"\n",
Expand All @@ -36,8 +36,8 @@
},
{
"cell_type": "markdown",
"metadata": {},
"id": "e016fbe7-e804-443f-8df8-f83795b0c6de",
"metadata": {},
"source": [
"## Configure the run\n",
"\n",
Expand All @@ -48,9 +48,9 @@
},
{
"cell_type": "code",
"metadata": {},
"id": "f9c82ae0-5561-4cad-8aab-6cc1467f9d50",
"execution_count": null,
"id": "f9c82ae0-5561-4cad-8aab-6cc1467f9d50",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
Expand All @@ -63,9 +63,9 @@
},
{
"cell_type": "code",
"metadata": {},
"id": "cf3b86c9-46ae-4c55-a591-7da19cceffc9",
"execution_count": null,
"id": "cf3b86c9-46ae-4c55-a591-7da19cceffc9",
"metadata": {},
"outputs": [],
"source": [
"!sweets config \\\n",
Expand All @@ -82,72 +82,72 @@
},
{
"cell_type": "code",
"metadata": {},
"id": "0e227531-9e75-42c5-a6cc-d088b7eea084",
"execution_count": null,
"id": "0e227531-9e75-42c5-a6cc-d088b7eea084",
"metadata": {},
"outputs": [],
"source": [
"!head -60 {CONFIG_YAML}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"id": "7be19beb-58bc-4170-a1fa-b468f38f2e40",
"metadata": {},
"source": [
"## Run the workflow\n",
"\n",
"Under the hood:\n",
"\n",
"1. sweets queries CMR for NISAR GSLC products on track 34 frame 18 that intersect the AOI, ranks the returned granules by (stack size, polarization match, frequency match), and picks the largest coherent signature group.\n",
"2. For each product in that group, sweets asks opera-utils for a bbox-subset of the HDF5 and verifies it actually has data in the AOI (NISAR PR products can advertise a frequency whose actual grid extent is narrower than the bounding polygon \u2014 those get skipped automatically and sweets falls through to the next signature).\n",
"2. For each product in that group, sweets asks opera-utils for a bbox-subset of the HDF5 and verifies it actually has data in the AOI (NISAR PR products can advertise a frequency whose actual grid extent is narrower than the bounding polygon those get skipped automatically and sweets falls through to the next signature).\n",
"3. Each successful subset gets a per-polarization VRT wrapper. dolphin opens those as normal rasters.\n",
"4. dolphin runs phase linking, interferogram network selection, SNAPHU unwrapping, timeseries inversion and velocity estimation as usual. The NISAR L-band wavelength is auto-detected from the HDF5 MODE code so outputs land in meters, not radians."
]
},
{
"cell_type": "code",
"metadata": {},
"id": "abfab05e-87c0-4913-be26-1e05d352b15c",
"execution_count": null,
"id": "abfab05e-87c0-4913-be26-1e05d352b15c",
"metadata": {},
"outputs": [],
"source": [
"!sweets run {CONFIG_YAML}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"id": "1cd06413-3951-4aed-b459-e8c0596cfde6",
"metadata": {},
"source": [
"## Inspect the outputs"
]
},
{
"cell_type": "code",
"metadata": {},
"id": "563d3f40-a8a4-40d7-9774-907ed64b8c10",
"execution_count": null,
"id": "563d3f40-a8a4-40d7-9774-907ed64b8c10",
"metadata": {},
"outputs": [],
"source": [
"!ls {WORK_DIR}/dolphin/"
]
},
{
"cell_type": "code",
"metadata": {},
"id": "b7ab7fc7-05c3-4fac-9c44-255be3656208",
"execution_count": null,
"id": "b7ab7fc7-05c3-4fac-9c44-255be3656208",
"metadata": {},
"outputs": [],
"source": [
"!ls {WORK_DIR}/dolphin/timeseries/"
]
},
{
"cell_type": "code",
"metadata": {},
"id": "83818ad7-4e59-49c8-ad23-7b52bb5e2e6a",
"execution_count": null,
"id": "83818ad7-4e59-49c8-ad23-7b52bb5e2e6a",
"metadata": {},
"outputs": [],
"source": [
"# Velocity raster: NISAR L-band, ~24 cm wavelength.\n",
Expand All @@ -156,8 +156,8 @@
},
{
"cell_type": "markdown",
"metadata": {},
"id": "909650a7-39eb-4c3b-bd20-821fb362eada",
"metadata": {},
"source": [
"## Under-the-hood: the VRT wrappers\n",
"\n",
Expand All @@ -166,24 +166,25 @@
},
{
"cell_type": "code",
"metadata": {},
"id": "d43e982f-8c61-4d89-b2c5-421c9d977605",
"execution_count": null,
"id": "d43e982f-8c61-4d89-b2c5-421c9d977605",
"metadata": {},
"outputs": [],
"source": [
"!ls -lh {WORK_DIR}/data/ 2>&1 | head -10"
]
},
{
"cell_type": "code",
"metadata": {},
"id": "5c15a4d7-440b-4711-92a8-b5ee636f88e8",
"execution_count": null,
"id": "5c15a4d7-440b-4711-92a8-b5ee636f88e8",
"metadata": {},
"outputs": [],
"source": [
"# Peek at one VRT to see how sweets injects the geotransform over\n",
"# the HDF5 subdataset.\n",
"import glob\n",
"\n",
"vrts = sorted(glob.glob(f\"{WORK_DIR}/data/*.vrt\"))\n",
"if vrts:\n",
" print(open(vrts[0]).read())"
Expand All @@ -197,9 +198,9 @@
"name": "python3"
},
"language_info": {
"name": "python",
"file_extension": ".py",
"mimetype": "text/x-python",
"file_extension": ".py"
"name": "python"
}
},
"nbformat": 4,
Expand Down
3 changes: 1 addition & 2 deletions scripts/ui.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@
"import sys\n",
"from datetime import datetime\n",
"\n",
"\n",
"import ipywidgets as widgets\n",
"import pydantic\n",
"from rich import print\n",
"from ipywidgets import GridBox, Layout\n",
"from rich import print\n",
"\n",
"from sweets.core import Workflow\n",
"\n",
Expand Down
Loading
Loading