diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f418dfc..18110ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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: diff --git a/docs/example_compare_sources.ipynb b/docs/example_compare_sources.ipynb index 3bc9883..897bf8d 100644 --- a/docs/example_compare_sources.ipynb +++ b/docs/example_compare_sources.ipynb @@ -2,8 +2,8 @@ "cells": [ { "cell_type": "markdown", - "metadata": {}, "id": "26faf28f-336d-4ced-9be8-4217ef2deea7", + "metadata": {}, "source": [ "# Cross-source comparison\n", "\n", @@ -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_\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_\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", @@ -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", @@ -93,8 +149,8 @@ }, { "cell_type": "markdown", - "metadata": {}, "id": "c9f60e41-bfc0-43f8-9c86-09e33e07440c", + "metadata": {}, "source": [ "## Effective velocity (meters / year)\n", "\n", @@ -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", @@ -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", @@ -178,9 +234,9 @@ "name": "python3" }, "language_info": { - "name": "python", + "file_extension": ".py", "mimetype": "text/x-python", - "file_extension": ".py" + "name": "python" } }, "nbformat": 4, diff --git a/docs/example_nisar.ipynb b/docs/example_nisar.ipynb index 2128f70..a1b7d97 100644 --- a/docs/example_nisar.ipynb +++ b/docs/example_nisar.ipynb @@ -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", @@ -18,8 +18,8 @@ }, { "cell_type": "markdown", - "metadata": {}, "id": "cbc5042b-98dd-455a-9523-f3d24cad15a1", + "metadata": {}, "source": [ "## Earthdata credentials\n", "\n", @@ -36,8 +36,8 @@ }, { "cell_type": "markdown", - "metadata": {}, "id": "e016fbe7-e804-443f-8df8-f83795b0c6de", + "metadata": {}, "source": [ "## Configure the run\n", "\n", @@ -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", @@ -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", @@ -82,9 +82,9 @@ }, { "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}" @@ -92,24 +92,24 @@ }, { "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}" @@ -117,17 +117,17 @@ }, { "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/" @@ -135,9 +135,9 @@ }, { "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/" @@ -145,9 +145,9 @@ }, { "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", @@ -156,8 +156,8 @@ }, { "cell_type": "markdown", - "metadata": {}, "id": "909650a7-39eb-4c3b-bd20-821fb362eada", + "metadata": {}, "source": [ "## Under-the-hood: the VRT wrappers\n", "\n", @@ -166,9 +166,9 @@ }, { "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" @@ -176,14 +176,15 @@ }, { "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())" @@ -197,9 +198,9 @@ "name": "python3" }, "language_info": { - "name": "python", + "file_extension": ".py", "mimetype": "text/x-python", - "file_extension": ".py" + "name": "python" } }, "nbformat": 4, diff --git a/scripts/ui.ipynb b/scripts/ui.ipynb index 63c4cb2..832dec6 100644 --- a/scripts/ui.ipynb +++ b/scripts/ui.ipynb @@ -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", diff --git a/src/sweets/_burst_db.py b/src/sweets/_burst_db.py index 612ab56..8b94e25 100644 --- a/src/sweets/_burst_db.py +++ b/src/sweets/_burst_db.py @@ -1,6 +1,5 @@ import zipfile from pathlib import Path -from typing import Optional import requests from loguru import logger @@ -8,10 +7,10 @@ from ._types import Filename from .utils import get_cache_dir -BURST_DB_URL = "https://github.com/scottstanie/burst_db/raw/frames-with-data/data/s1-frames-9frames-5min-10max-bbox-only.gpkg.zip" # noqa: E501 +BURST_DB_URL = "https://github.com/scottstanie/burst_db/raw/frames-with-data/data/s1-frames-9frames-5min-10max-bbox-only.gpkg.zip" -def get_burst_db(url: str = BURST_DB_URL, out_file: Optional[Filename] = None) -> Path: +def get_burst_db(url: str = BURST_DB_URL, out_file: Filename | None = None) -> Path: """Read or download the burst-db file. TODO: getting a URL to a release version of the sqlite database. diff --git a/src/sweets/_dolphin.py b/src/sweets/_dolphin.py index d51f1ba..eb5c7dc 100644 --- a/src/sweets/_dolphin.py +++ b/src/sweets/_dolphin.py @@ -21,11 +21,10 @@ from math import cos, floor, log2, radians from pathlib import Path -from typing import TYPE_CHECKING, Literal, Optional - -from pydantic import BaseModel, Field +from typing import TYPE_CHECKING, Literal from loguru import logger +from pydantic import BaseModel, Field from ._log import log_runtime @@ -149,11 +148,11 @@ def build_displacement_config( cslc_files: list[Path], work_directory: Path, *, - options: Optional[DolphinOptions] = None, - mask_file: Optional[Path] = None, - bounds: Optional[tuple[float, float, float, float]] = None, + options: DolphinOptions | None = None, + mask_file: Path | None = None, + bounds: tuple[float, float, float, float] | None = None, subdataset: str = "/data/VV", - wavelength: Optional[float] = None, + wavelength: float | None = None, ): """Build a :class:`DisplacementWorkflow` config from sweets options. @@ -266,13 +265,13 @@ def run_displacement( cslc_files: list[Path], work_directory: Path, *, - options: Optional[DolphinOptions] = None, - mask_file: Optional[Path] = None, - bounds: Optional[tuple[float, float, float, float]] = None, - config_yaml: Optional[Path] = None, + options: DolphinOptions | None = None, + mask_file: Path | None = None, + bounds: tuple[float, float, float, float] | None = None, + config_yaml: Path | None = None, subdataset: str = "/data/VV", - wavelength: Optional[float] = None, -) -> "OutputPaths": + wavelength: float | None = None, +) -> OutputPaths: """Build the dolphin config and run the displacement workflow. Parameters diff --git a/src/sweets/_geocode_slcs.py b/src/sweets/_geocode_slcs.py index 86b2ffc..c4936a5 100644 --- a/src/sweets/_geocode_slcs.py +++ b/src/sweets/_geocode_slcs.py @@ -3,7 +3,7 @@ import shutil from os import fspath from pathlib import Path -from typing import List, Literal, Optional, Tuple +from typing import Literal import compass.s1_geocode_slc import compass.s1_static_layers @@ -11,7 +11,6 @@ import yaml # type: ignore[import-untyped] from compass import s1_geocode_stack from compass.utils.geo_runconfig import GeoRunConfig - from loguru import logger from ._types import Filename @@ -104,7 +103,7 @@ def create_config_files( burst_db_file: Filename, dem_file: Filename, orbit_dir: Filename, - bbox: Optional[Tuple[float, ...]] = None, + bbox: tuple[float, ...] | None = None, x_posting: float = 5, y_posting: float = 10, pol_type: str = "co-pol", @@ -113,7 +112,7 @@ def create_config_files( using_zipped: bool = False, gpu_enabled: bool = True, gpu_id: int = 0, -) -> List[Path]: +) -> list[Path]: """Create the geocoding config files for a stack of SLCs. Parameters @@ -216,7 +215,7 @@ def _resolve_gpu_enabled(requested: bool) -> bool: def _patch_worker_settings( - runconfig_files: List[Path], *, gpu_enabled: bool, gpu_id: int + runconfig_files: list[Path], *, gpu_enabled: bool, gpu_id: int ) -> None: """Overwrite ``runconfig.groups.worker`` in each runconfig YAML. diff --git a/src/sweets/_geometry.py b/src/sweets/_geometry.py index 55e0777..55f64f4 100644 --- a/src/sweets/_geometry.py +++ b/src/sweets/_geometry.py @@ -1,14 +1,12 @@ from __future__ import annotations from pathlib import Path -from typing import Optional import rasterio as rio from dolphin import io, stitching from dolphin._types import Bbox -from opera_utils import group_by_burst - from loguru import logger +from opera_utils import group_by_burst from ._types import Filename @@ -18,7 +16,7 @@ def stitch_geometry( geom_dir: Path, dem_filename: Filename, looks: tuple[int, int], - bbox: Optional[Bbox] = None, + bbox: Bbox | None = None, overwrite: bool = False, ): """Stitch the burst-wise geometry files. diff --git a/src/sweets/_orbit.py b/src/sweets/_orbit.py index 5d3079f..809582f 100644 --- a/src/sweets/_orbit.py +++ b/src/sweets/_orbit.py @@ -1,11 +1,10 @@ from pathlib import Path -from typing import List from eof import download from loguru import logger -def download_orbits(search_path: Path, save_dir: Path) -> List[Path]: +def download_orbits(search_path: Path, save_dir: Path) -> list[Path]: """Download orbit files for a given search path.""" logger.info(f"Orbit search_path: {search_path}, save_dir: {save_dir}") filenames = download.main( diff --git a/src/sweets/_report.py b/src/sweets/_report.py index bfea108..9ebec14 100644 --- a/src/sweets/_report.py +++ b/src/sweets/_report.py @@ -24,7 +24,7 @@ from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from loguru import logger @@ -47,7 +47,7 @@ class _Section: def build_report( config_file: Path, - output: Optional[Path] = None, + output: Path | None = None, ) -> Path: """Render an HTML report for a completed sweets run. @@ -100,7 +100,7 @@ def build_report( # --------------------------------------------------------------------------- -def _build_header(workflow: "Workflow", config_path: Path) -> _Section: +def _build_header(workflow: Workflow, config_path: Path) -> _Section: rows: list[tuple[str, str]] = [ ("Work directory", str(workflow.work_dir)), ("Config file", str(config_path)), @@ -145,13 +145,13 @@ def _build_raster_section(dolphin_dir: Path, kind: str) -> _Section: ts_dir = dolphin_dir / "timeseries" ifg_dir = dolphin_dir / "interferograms" - path: Optional[Path] = None + path: Path | None = None title = kind cbar_label = "" cmap = "RdBu_r" diverging = True clip_nodata_zero = False - fixed_vlim: Optional[tuple[float, float]] = None + fixed_vlim: tuple[float, float] | None = None if kind == "velocity": path = ts_dir / "velocity.tif" @@ -300,7 +300,7 @@ def _build_inventory(dolphin_dir: Path) -> _Section: # Group by parent dir for readability out = "\n" out += "\n" - current_parent: Optional[str] = None + current_parent: str | None = None for rel, size in rows: parent = str(rel.parent) if parent != current_parent: @@ -327,7 +327,7 @@ def _build_inventory(dolphin_dir: Path) -> _Section: def _read_raster( path: Path, *, default_unit: str = "" -) -> tuple[Any, tuple[float, float, float, float], Optional[float], str]: +) -> tuple[Any, tuple[float, float, float, float], float | None, str]: """Read a single-band raster via GDAL and return (data, bounds, nodata, unit). Uses GDAL directly because rasterio's ``_band_dtype`` map doesn't @@ -367,9 +367,9 @@ def _read_raster( return data, (left, bottom, right, top), nodata, unit -def _longest_timeseries_pair(ts_dir: Path) -> Optional[tuple[Path, datetime, datetime]]: +def _longest_timeseries_pair(ts_dir: Path) -> tuple[Path, datetime, datetime] | None: """Return the `YYYYMMDD_YYYYMMDD.tif` timeseries step with the widest baseline.""" - best: Optional[tuple[Path, datetime, datetime]] = None + best: tuple[Path, datetime, datetime] | None = None best_span = -1 for p in ts_dir.glob("*.tif"): m = _PAIR_RE.match(p.name) @@ -394,7 +394,7 @@ def _truncate(s: str, n: int) -> str: return s if len(s) <= n else s[: n - 3] + "..." -def _wall_time(work_dir: Path) -> Optional[str]: +def _wall_time(work_dir: Path) -> str | None: """Estimate wall time from log mtimes in work_dir / dolphin / .""" dolphin_dir = work_dir / "dolphin" candidates = list(dolphin_dir.rglob("*.tif")) @@ -409,7 +409,7 @@ def _wall_time(work_dir: Path) -> Optional[str]: return f"{span / 3600:.1f} h" -def _dolphin_version() -> Optional[str]: +def _dolphin_version() -> str | None: try: import dolphin @@ -448,13 +448,13 @@ def _render_raster_png( diverging: bool, cbar_label: str, title: str, - fixed_vlim: Optional[tuple[float, float]] = None, + fixed_vlim: tuple[float, float] | None = None, ) -> str: import matplotlib.pyplot as plt import numpy as np - vmin: Optional[float] - vmax: Optional[float] + vmin: float | None + vmax: float | None if fixed_vlim is not None: vmin, vmax = fixed_vlim else: @@ -487,7 +487,7 @@ def _render_raster_png( return png -def _fig_to_png(fig) -> str: # noqa: ANN001 +def _fig_to_png(fig) -> str: buf = io.BytesIO() fig.savefig(buf, format="png", dpi=120, bbox_inches="tight") return base64.b64encode(buf.getvalue()).decode("ascii") diff --git a/src/sweets/_show_versions.py b/src/sweets/_show_versions.py index dc2c2e5..af6554c 100644 --- a/src/sweets/_show_versions.py +++ b/src/sweets/_show_versions.py @@ -10,7 +10,6 @@ import importlib import platform import sys -from typing import Optional import sweets @@ -32,7 +31,7 @@ def _get_sys_info() -> dict[str, str]: } -def _get_opera_info() -> dict[str, Optional[str]]: +def _get_opera_info() -> dict[str, str | None]: """Information on system on core modules. Returns @@ -50,7 +49,7 @@ def _get_opera_info() -> dict[str, Optional[str]]: return blob -def _get_version(module_name: str) -> Optional[str]: +def _get_version(module_name: str) -> str | None: if module_name in sys.modules: mod = sys.modules[module_name] else: @@ -64,7 +63,7 @@ def _get_version(module_name: str) -> Optional[str]: return mod.version -def _get_deps_info() -> dict[str, Optional[str]]: +def _get_deps_info() -> dict[str, str | None]: """Overview of the installed version of main dependencies. Returns diff --git a/src/sweets/_tropo.py b/src/sweets/_tropo.py index 580da40..ec64c93 100644 --- a/src/sweets/_tropo.py +++ b/src/sweets/_tropo.py @@ -29,22 +29,18 @@ import warnings from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Literal, Optional +from typing import Literal import h5py import numpy as np import rasterio import rioxarray as rxr +from loguru import logger from pydantic import BaseModel, Field from shapely import wkt as shp_wkt -from loguru import logger - from ._log import log_runtime -if TYPE_CHECKING: - pass - # Sentinel-1 C-band carrier wavelength used by OPERA CSLCs and burst2safe # SAFEs alike. S1_WAVELENGTH_M = 0.05546576 @@ -172,7 +168,7 @@ def create_tropo_corrections( dem_path: Path, incidence_angle_path: Path, output_dir: Path, - options: Optional[TropoOptions] = None, + options: TropoOptions | None = None, ) -> list[Path]: """Run the OPERA tropo workflow over a stack of SLC files. @@ -216,7 +212,7 @@ def create_tropo_corrections( ) -def _parse_tropo_filename(p: Path) -> Optional[datetime]: +def _parse_tropo_filename(p: Path) -> datetime | None: """Return the datetime encoded in a tropo correction filename.""" m = _TROPO_FILENAME_RE.match(p.name) if not m: @@ -245,7 +241,7 @@ def _group_tropo_files_by_date(tropo_files: list[Path]) -> dict[str, list[Path]] return out -def _mean_tropo_on_grid(tropo_files: list[Path], target) -> np.ndarray: # noqa: ANN001 +def _mean_tropo_on_grid(tropo_files: list[Path], target) -> np.ndarray: """Reproject a list of tropo rasters onto `target` and return the mean. `target` is an xarray DataArray that carries the desired grid + CRS @@ -260,7 +256,7 @@ def _mean_tropo_on_grid(tropo_files: list[Path], target) -> np.ndarray: # noqa: return np.nanmean(np.stack(stack, axis=0), axis=0) -def _ifg_dates(path: Path) -> Optional[tuple[str, str]]: +def _ifg_dates(path: Path) -> tuple[str, str] | None: """Pull the (date1, date2) pair out of a `_*.tif` name.""" m = re.match(r"(\d{8})_(\d{8})", path.name) if not m: @@ -276,7 +272,7 @@ def _apply_one_pair( tropo_by_date: dict[str, list[Path]], output_path: Path, scale: float, -) -> Optional[Path]: +) -> Path | None: """Subtract the per-date differential tropo from one raster. ``scale`` is applied to the metres-of-LOS-delay difference before @@ -396,7 +392,7 @@ def run_tropo_correction( dem_path: Path, incidence_angle_path: Path, dolphin_work_dir: Path, - options: Optional[TropoOptions] = None, + options: TropoOptions | None = None, wavelength: float = S1_WAVELENGTH_M, ) -> list[Path]: """Build tropo corrections + apply to dolphin's unwrapped and timeseries. diff --git a/src/sweets/_water_mask.py b/src/sweets/_water_mask.py index 70b5068..f3ba8bd 100644 --- a/src/sweets/_water_mask.py +++ b/src/sweets/_water_mask.py @@ -21,14 +21,12 @@ from enum import Enum from os import fspath from pathlib import Path -from typing import Optional import numpy as np +from loguru import logger from osgeo import gdal from scipy import ndimage -from loguru import logger - gdal.UseExceptions() TILE_URL_BASE = "https://asf-dem-west.s3.amazonaws.com/WATER_MASK/TILES/" @@ -103,7 +101,7 @@ def _buffer_mask( def create_water_mask( bounds: tuple[float, float, float, float], output: Path, - resolution: Optional[float] = None, + resolution: float | None = None, buffer_meters: float = 0.0, water_value: WaterValue = WaterValue.ZERO, overwrite: bool = False, diff --git a/src/sweets/cli.py b/src/sweets/cli.py index 7a2ad24..e5d1d5f 100644 --- a/src/sweets/cli.py +++ b/src/sweets/cli.py @@ -32,7 +32,7 @@ import sys from dataclasses import dataclass from pathlib import Path -from typing import Annotated, Any, Literal, Optional +from typing import Annotated, Any, Literal import tyro from pydantic import Field, model_validator @@ -70,7 +70,7 @@ class ConfigCli(Workflow): # Optional one in the subclass. A non-empty description is required # so dolphin's YAML-comment writer (which walks the full schema, # including excluded fields) doesn't trip over a missing key. - search: Annotated[Optional[Source], tyro.conf.Suppress] = Field( # type: ignore[assignment] + search: Annotated[Source | None, tyro.conf.Suppress] = Field( # type: ignore[assignment] default=None, description=( "Source of input SLCs. Built by `_assemble_search` from the" @@ -85,14 +85,14 @@ class ConfigCli(Workflow): # so it can't call those factories — we override with plain # `Optional[Path] = None` and the wrap validator strips them when # unset so Workflow's own factory takes over downstream. - dem_filename: Optional[Path] = Field( # type: ignore[assignment] + dem_filename: Path | None = Field( # type: ignore[assignment] default=None, description=( "DEM raster in EPSG:4326. Defaults to `/dem.tif`" " (downloaded via sardem)." ), ) - water_mask_filename: Optional[Path] = Field( # type: ignore[assignment] + water_mask_filename: Path | None = Field( # type: ignore[assignment] default=None, description=( "Water mask in EPSG:4326 (uint8, 1=land, 0=water). Defaults" @@ -102,7 +102,7 @@ class ConfigCli(Workflow): # --- Source-flat flags (folded into `search`, not dumped) --- - start: Optional[str] = Field( + start: str | None = Field( default=None, description=( "Start date for the search (YYYY-MM-DD). Required for `safe`," @@ -111,7 +111,7 @@ class ConfigCli(Workflow): ), exclude=True, ) - end: Optional[str] = Field( + end: str | None = Field( default=None, description=( "End date for the search (YYYY-MM-DD). Required for `safe`," @@ -131,7 +131,7 @@ class ConfigCli(Workflow): ), exclude=True, ) - track: Optional[int] = Field( + track: int | None = Field( default=None, description=( "Relative orbit / track number. Required for --source safe;" @@ -141,7 +141,7 @@ class ConfigCli(Workflow): ), exclude=True, ) - frame: Optional[int] = Field( + frame: int | None = Field( default=None, description=( "NISAR track-frame number — the `Frame` field on ASF Vertex" @@ -166,7 +166,7 @@ class ConfigCli(Workflow): ), exclude=True, ) - swaths: Optional[list[str]] = Field( + swaths: list[str] | None = Field( default=None, description=( "Restrict to specific subswaths (e.g. ['IW2']). Only" @@ -352,7 +352,7 @@ class ReportCmd: containing a sweets_config.yaml is also accepted and resolved to the yaml inside it.""" - output: Optional[Path] = None + output: Path | None = None """Where to write the report. Defaults to `/sweets_report.html`.""" def execute(self) -> None: diff --git a/src/sweets/core.py b/src/sweets/core.py index 837b61d..080bdf5 100644 --- a/src/sweets/core.py +++ b/src/sweets/core.py @@ -20,16 +20,15 @@ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, wait from functools import partial from pathlib import Path -from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, Union +from typing import TYPE_CHECKING, Annotated, Any, Literal from dolphin.utils import set_num_threads from dolphin.workflows.config import YamlModel +from loguru import logger from opera_utils import group_by_burst from pydantic import ConfigDict, Field, computed_field, field_validator, model_validator from shapely import wkt as shp_wkt -from loguru import logger - from ._burst_db import get_burst_db from ._dolphin import DolphinOptions, run_displacement from ._geocode_slcs import create_config_files, run_geocode, run_static_layers @@ -49,7 +48,7 @@ # the matching variant — much cleaner errors than a plain Union, which # tries each variant in order and reports failures from all of them. Source = Annotated[ - Union[BurstSearch, LocalSafeSearch, OperaCslcSearch, NisarGslcSearch], + BurstSearch | LocalSafeSearch | OperaCslcSearch | NisarGslcSearch, Field(discriminator="kind"), ] @@ -63,14 +62,14 @@ class Workflow(YamlModel): validate_default=True, ) - bbox: Optional[tuple[float, float, float, float]] = Field( + bbox: tuple[float, float, float, float] | None = Field( default=None, description=( "AOI as (left, bottom, right, top) in decimal degrees. Either" " `bbox` or `wkt` must be set." ), ) - wkt: Optional[str] = Field( + wkt: str | None = Field( default=None, description="AOI as a WKT polygon (or path to a `.wkt` file). Overrides bbox.", ) @@ -93,7 +92,7 @@ class Workflow(YamlModel): " Copernicus DEM via sardem." ), ) - dem_bbox: Optional[tuple[float, float, float, float]] = Field( + dem_bbox: tuple[float, float, float, float] | None = Field( default=None, description=( "Optional AOI override for DEM download (left, bottom, right," @@ -242,7 +241,7 @@ def _sync_aoi(cls, values: Any) -> Any: return values @model_validator(mode="after") - def _set_bbox_and_wkt(self) -> "Workflow": + def _set_bbox_and_wkt(self) -> Workflow: # Derive bbox from wkt if only wkt was supplied; downstream code # (DEM, dolphin bounds, etc.) all reads bbox, not wkt. We do NOT # auto-fill wkt from bbox: nothing in the workflow reads outer wkt @@ -346,7 +345,7 @@ def save(self, config_file: Filename = "sweets_config.yaml") -> None: self.to_yaml(config_file) @classmethod - def load(cls, config_file: Filename = "sweets_config.yaml") -> "Workflow": + def load(cls, config_file: Filename = "sweets_config.yaml") -> Workflow: """Load a configuration from a YAML file.""" logger.info(f"Loading config from {config_file}") return cls.from_yaml(config_file) @@ -651,7 +650,7 @@ def _dolphin_subdataset(self) -> str: return "/unused-for-raster-inputs" return "/data/VV" - def _dolphin_wavelength(self) -> Optional[float]: + def _dolphin_wavelength(self) -> float | None: """Wavelength override for dolphin, or None to let dolphin auto-detect. For the NISAR source, sweets reads the precise carrier from the @@ -667,7 +666,7 @@ def _dolphin_wavelength(self) -> Optional[float]: return None @log_runtime - def _run_dolphin(self, gslc_files: list[Path]) -> "OutputPaths": + def _run_dolphin(self, gslc_files: list[Path]) -> OutputPaths: mask = self.water_mask_filename if self.water_mask_filename.exists() else None return run_displacement( cslc_files=gslc_files, @@ -685,7 +684,7 @@ def _run_dolphin(self, gslc_files: list[Path]) -> "OutputPaths": # ------------------------------------------------------------------ @log_runtime - def run(self, starting_step: int = 1) -> "OutputPaths": + def run(self, starting_step: int = 1) -> OutputPaths: """Run the full workflow. Parameters @@ -818,7 +817,7 @@ def run(self, starting_step: int = 1) -> "OutputPaths": @log_runtime def _run_tropo( - self, gslc_files: list[Path], out_paths: "OutputPaths" + self, gslc_files: list[Path], out_paths: OutputPaths ) -> list[Path]: """Apply OPERA L4 TROPO-ZENITH corrections to dolphin's outputs. diff --git a/src/sweets/dem.py b/src/sweets/dem.py index f6f8b0a..1df70f6 100644 --- a/src/sweets/dem.py +++ b/src/sweets/dem.py @@ -2,7 +2,6 @@ from os import fspath from pathlib import Path -from typing import Tuple import sardem.dem from loguru import logger @@ -15,7 +14,7 @@ @log_runtime -def create_dem(output_name: Filename, bbox: Tuple[float, float, float, float]) -> Path: +def create_dem(output_name: Filename, bbox: tuple[float, float, float, float]) -> Path: """Download a Copernicus Global DEM clipped to `bbox`.""" output_name = Path(output_name).resolve() if output_name.exists(): @@ -36,7 +35,7 @@ def create_dem(output_name: Filename, bbox: Tuple[float, float, float, float]) - @log_runtime def create_water_mask( output_name: Path, - bbox: Tuple[float, float, float, float], + bbox: tuple[float, float, float, float], buffer_meters: float = 0.0, ) -> Path: """Create a high-resolution binary land(1) / water(0) mask. diff --git a/src/sweets/download.py b/src/sweets/download.py index d08f681..4176e9f 100644 --- a/src/sweets/download.py +++ b/src/sweets/download.py @@ -30,19 +30,19 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor from datetime import date, datetime from pathlib import Path -from typing import Any, Callable, Literal, Optional, TypeVar +from typing import Any, Literal, TypeVar from dateutil.parser import parse as parse_date from dolphin.workflows.config import YamlModel +from loguru import logger from pydantic import ConfigDict, Field, field_validator, model_validator from shapely import wkt as shp_wkt from shapely.geometry import Polygon, box -from loguru import logger - from ._log import log_runtime _T = TypeVar("_T") @@ -87,14 +87,14 @@ class BurstSearch(YamlModel): description="Directory where SAFE directories will be written.", validate_default=True, ) - bbox: Optional[tuple[float, float, float, float]] = Field( + bbox: tuple[float, float, float, float] | None = Field( None, description=( "Area of interest as (left, bottom, right, top) in decimal degrees." " Either `bbox` or `wkt` must be set." ), ) - wkt: Optional[str] = Field( + wkt: str | None = Field( None, description=( "Area of interest as a WKT polygon string (or path to a `.wkt` file)." @@ -109,12 +109,12 @@ class BurstSearch(YamlModel): default_factory=datetime.now, description="Search end time. Defaults to now.", ) - track: Optional[int] = Field( + track: int | None = Field( None, alias="relativeOrbit", description="Sentinel-1 relative orbit / track number.", ) - flight_direction: Optional[FlightDirection] = Field( + flight_direction: FlightDirection | None = Field( None, alias="flightDirection", description="Restrict to ASCENDING or DESCENDING acquisitions.", @@ -123,7 +123,7 @@ class BurstSearch(YamlModel): default_factory=lambda: ["VV"], description="Polarizations to include (e.g. ['VV'], ['VV', 'VH']).", ) - swaths: Optional[list[str]] = Field( + swaths: list[str] | None = Field( None, description=( "Restrict to specific subswaths (e.g. ['IW2']). If None, all swaths" @@ -166,7 +166,7 @@ def _absolute_out_dir(cls, v: Path) -> Path: @field_validator("flight_direction", mode="before") @classmethod - def _normalize_flight_direction(cls, v: Any) -> Optional[str]: + def _normalize_flight_direction(cls, v: Any) -> str | None: if v is None: return None s = str(v).upper() @@ -184,11 +184,11 @@ def _upper_pols(cls, v: list[str]) -> list[str]: @field_validator("swaths") @classmethod - def _upper_swaths(cls, v: Optional[list[str]]) -> Optional[list[str]]: + def _upper_swaths(cls, v: list[str] | None) -> list[str] | None: return [s.upper() for s in v] if v else v @model_validator(mode="after") - def _check_aoi_and_dates(self) -> "BurstSearch": + def _check_aoi_and_dates(self) -> BurstSearch: if not self.wkt and not self.bbox: msg = "Must provide either `bbox` or `wkt`" raise ValueError(msg) @@ -335,14 +335,14 @@ class LocalSafeSearch(YamlModel): " one ``S1[AB]_*.SAFE`` or ``S1[AB]_*.zip`` file." ), ) - bbox: Optional[tuple[float, float, float, float]] = Field( + bbox: tuple[float, float, float, float] | None = Field( None, description=( "Area of interest as (left, bottom, right, top) in decimal degrees." " Either `bbox` or `wkt` must be set." ), ) - wkt: Optional[str] = Field( + wkt: str | None = Field( None, description=( "Area of interest as a WKT polygon string (or path to a `.wkt` file)." @@ -362,7 +362,7 @@ def _absolute_out_dir(cls, v: Path) -> Path: return Path(v).expanduser().resolve() @model_validator(mode="after") - def _check_aoi(self) -> "LocalSafeSearch": + def _check_aoi(self) -> LocalSafeSearch: if not self.wkt and not self.bbox: msg = "Must provide either `bbox` or `wkt`" raise ValueError(msg) @@ -440,14 +440,14 @@ class OperaCslcSearch(YamlModel): ), validate_default=True, ) - bbox: Optional[tuple[float, float, float, float]] = Field( + bbox: tuple[float, float, float, float] | None = Field( None, description=( "Area of interest as (left, bottom, right, top) in decimal degrees." " Either `bbox` or `wkt` must be set." ), ) - wkt: Optional[str] = Field( + wkt: str | None = Field( None, description=( "Area of interest as a WKT polygon string (or path to a `.wkt` file)." @@ -461,12 +461,12 @@ class OperaCslcSearch(YamlModel): default_factory=datetime.now, description="Search end time. Defaults to now.", ) - track: Optional[int] = Field( + track: int | None = Field( None, alias="relativeOrbit", description="Sentinel-1 relative orbit / track number.", ) - burst_ids: Optional[list[str]] = Field( + burst_ids: list[str] | None = Field( None, description=( "Restrict to specific OPERA burst IDs (e.g. ['t078_165573_iw2']);" @@ -500,7 +500,7 @@ def _absolute_out_dir(cls, v: Path) -> Path: return Path(v).expanduser().resolve() @model_validator(mode="after") - def _check_aoi_and_dates(self) -> "OperaCslcSearch": + def _check_aoi_and_dates(self) -> OperaCslcSearch: if not self.wkt and not self.bbox: msg = "Must provide either `bbox` or `wkt`" raise ValueError(msg) @@ -665,7 +665,7 @@ class NisarGslcSearch(YamlModel): description="Directory where the NISAR GSLC HDF5s will be written.", validate_default=True, ) - bbox: Optional[tuple[float, float, float, float]] = Field( + bbox: tuple[float, float, float, float] | None = Field( None, description=( "Area of interest as (left, bottom, right, top) in decimal degrees." @@ -673,7 +673,7 @@ class NisarGslcSearch(YamlModel): " or `wkt` must be set." ), ) - wkt: Optional[str] = Field( + wkt: str | None = Field( None, description=( "Area of interest as a WKT polygon string (or path to a `.wkt`" @@ -688,7 +688,7 @@ class NisarGslcSearch(YamlModel): default_factory=datetime.now, description="Search end time. Defaults to now.", ) - track: Optional[int] = Field( + track: int | None = Field( None, alias="relative_orbit_number", description=( @@ -698,7 +698,7 @@ class NisarGslcSearch(YamlModel): " repeat-pass stack." ), ) - frame: Optional[int] = Field( + frame: int | None = Field( None, alias="track_frame_number", description=( @@ -707,7 +707,7 @@ class NisarGslcSearch(YamlModel): " across repeat passes." ), ) - frequency: Optional[Literal["A", "B"]] = Field( + frequency: Literal["A", "B"] | None = Field( default=None, description=( "NISAR frequency band: `A` (L-band primary) or `B`. If left as" @@ -718,7 +718,7 @@ class NisarGslcSearch(YamlModel): " usually wrong." ), ) - polarizations: Optional[list[str]] = Field( + polarizations: list[str] | None = Field( None, description=( "Polarizations to keep (e.g. ['HH']). If left as the default" @@ -758,11 +758,11 @@ def _absolute_out_dir(cls, v: Path) -> Path: @field_validator("polarizations") @classmethod - def _upper_pols(cls, v: Optional[list[str]]) -> Optional[list[str]]: + def _upper_pols(cls, v: list[str] | None) -> list[str] | None: return [p.upper() for p in v] if v else v @model_validator(mode="after") - def _check_aoi_and_dates(self) -> "NisarGslcSearch": + def _check_aoi_and_dates(self) -> NisarGslcSearch: if not self.wkt and not self.bbox: msg = "Must provide either `bbox` or `wkt`" raise ValueError(msg) @@ -968,11 +968,11 @@ def download(self) -> list[Path]: def _download_group( self, - chosen: list, # noqa: ANN001 + chosen: list, chosen_freq: str, chosen_pols: list[str], bounds: tuple[float, float, float, float], - process_file, # noqa: ANN001 + process_file, ) -> list[Path]: """Download + GeoTIFF-convert every product in one signature group.""" outputs: list[Path] = [] @@ -1090,7 +1090,6 @@ def wavelength(self) -> float: timeseries outputs in meters instead of radians. """ import h5py - from dolphin import constants from dolphin.constants import SPEED_OF_LIGHT @@ -1098,7 +1097,7 @@ def wavelength(self) -> float: candidates = h5_files if h5_files else self.existing_files() assert candidates, f"No NISAR files in {self.out_dir}; run download() first" - chosen_letter: Optional[str] = None + chosen_letter: str | None = None if h5_files: with h5py.File(h5_files[0], "r") as hf: for freq_letter in ("A", "B"): @@ -1142,7 +1141,7 @@ def wavelength(self) -> float: def _group_nisar_results_by_signature( - results, # noqa: ANN001 + results, ) -> dict[tuple[str, frozenset[str]], list]: """Group NISAR search results by (frequency_letter, frozenset(pols)). @@ -1165,10 +1164,10 @@ def _group_nisar_results_by_signature( def _get_per_product_rowcol_slice( - product, # noqa: ANN001 + product, bbox: tuple[float, float, float, float], frequency: str, -) -> tuple[Optional[slice], Optional[slice]]: +) -> tuple[slice | None, slice | None]: """Compute row/col slices for `bbox` against this product's own grid. opera-utils' default `_get_rowcol_slice` uses results[0]'s grid for @@ -1284,7 +1283,7 @@ def _nisar_h5_to_vrts( return out_paths -def _peek_nisar_grid_from_handle(hf) -> tuple[str, list[str]]: # noqa: ANN001 +def _peek_nisar_grid_from_handle(hf) -> tuple[str, list[str]]: """Inspect an open NISAR GSLC HDF5 file handle for grid layout.""" grids_path = "/science/LSAR/GSLC/grids" if grids_path not in hf: diff --git a/src/sweets/plotting.py b/src/sweets/plotting.py index b0ba353..a3fc720 100644 --- a/src/sweets/plotting.py +++ b/src/sweets/plotting.py @@ -1,7 +1,7 @@ from __future__ import annotations +from collections.abc import Sequence from pathlib import Path -from typing import Optional, Sequence, Tuple, Union import cartopy.crs as ccrs import cartopy.feature as cfeature @@ -46,15 +46,15 @@ def plot_ifg( - img: Optional[ArrayLike] = None, - filename: Optional[Filename] = None, + img: ArrayLike | None = None, + filename: Filename | None = None, phase_cmap: str = "oil_slick", - ax: Optional[plt.Axes] = None, + ax: plt.Axes | None = None, add_colorbar: bool = True, title: str = "", - figsize: Optional[tuple[float, float]] = None, + figsize: tuple[float, float] | None = None, plot_cor: bool = False, - subsample_factor: Union[int, tuple[int, int]] = 1, + subsample_factor: int | tuple[int, int] = 1, **kwargs, ): """Plot an interferogram. @@ -141,21 +141,21 @@ def _plot_cc( def browse_ifgs( - sweets_path: Optional[Filename] = None, - file_list: Optional[Sequence[Filename]] = None, - cor_list: Optional[Sequence[Filename]] = None, - unw_list: Optional[Sequence[Filename]] = None, - conncomp_list: Optional[Sequence[Filename]] = None, - amp_image: Optional[ArrayLike] = None, + sweets_path: Filename | None = None, + file_list: Sequence[Filename] | None = None, + cor_list: Sequence[Filename] | None = None, + unw_list: Sequence[Filename] | None = None, + conncomp_list: Sequence[Filename] | None = None, + amp_image: ArrayLike | None = None, figsize: tuple[int, int] = (7, 4), vm_unw: float = 10, vm_cor: float = 1, unw_suffix: str = ".unw.tif", layout="box", - axes: Optional[plt.Axes] = None, - ref_unw: Optional[tuple[float, float]] = None, - overview: Optional[int] = None, - subsample_factor: Union[int, tuple[int, int]] = 1, + axes: plt.Axes | None = None, + ref_unw: tuple[float, float] | None = None, + overview: int | None = None, + subsample_factor: int | tuple[int, int] = 1, ): """Browse interferograms in a sweets directory. @@ -366,13 +366,13 @@ def browse_plot(idx=0): def plot_area_of_interest( - state: Optional[str] = None, - bbox: Optional[Tuple[float, float, float, float]] = None, - area_coordinates: Optional[Sequence[Tuple[float, float]]] = None, + state: str | None = None, + bbox: tuple[float, float, float, float] | None = None, + area_coordinates: Sequence[tuple[float, float]] | None = None, buffer: float = 0.0, - grid_step: Optional[float] = 1.0, + grid_step: float | None = 1.0, ax=None, -) -> Tuple: +) -> tuple: """Make a basic map to highlight an area of interest. Parameters diff --git a/src/sweets/utils.py b/src/sweets/utils.py index 2390c36..54c9560 100644 --- a/src/sweets/utils.py +++ b/src/sweets/utils.py @@ -3,7 +3,6 @@ import json import os from pathlib import Path -from typing import Optional, Tuple import rasterio as rio from rasterio.vrt import WarpedVRT @@ -43,7 +42,7 @@ def to_wkt(geojson: str) -> str: return wkt.dumps(geometry.shape(json.loads(geojson))) -def to_bbox(*, geojson: Optional[str] = None, wkt_str: Optional[str] = None) -> Tuple: +def to_bbox(*, geojson: str | None = None, wkt_str: str | None = None) -> tuple: """Convert a geojson or WKT string to a bounding box. Parameters @@ -72,7 +71,7 @@ def to_bbox(*, geojson: Optional[str] = None, wkt_str: Optional[str] = None) -> return tuple(geom.bounds) -def get_transformed_bounds(filename: Filename, epsg_code: Optional[int] = None): +def get_transformed_bounds(filename: Filename, epsg_code: int | None = None): """Get the bounds of a raster, possibly in a different CRS. Parameters @@ -98,7 +97,7 @@ def get_transformed_bounds(filename: Filename, epsg_code: Optional[int] = None): def get_intersection_bounds( fname1: Filename, fname2: Filename, epsg_code: int = 4326 -) -> Tuple[float, float, float, float]: +) -> tuple[float, float, float, float]: """Find the (left, bot, right, top) bounds of the raster intersection. Parameters @@ -123,8 +122,8 @@ def get_intersection_bounds( def get_overlapping_bounds( - bbox1: Tuple[float, float, float, float], bbox2: Tuple[float, float, float, float] -) -> Tuple[float, float, float, float]: + bbox1: tuple[float, float, float, float], bbox2: tuple[float, float, float, float] +) -> tuple[float, float, float, float]: """Find the (left, bot, right, top) bounds of the bbox intersection. Parameters diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 5e75ae8..de94bd1 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -12,11 +12,11 @@ matplotlib.use("Agg") -import matplotlib as mpl # noqa: E402 -import matplotlib.pyplot as plt # noqa: E402 -import numpy as np # noqa: E402 +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np -from sweets import plotting # noqa: E402 +from sweets import plotting def test_oil_slick_registered_with_matplotlib() -> None:
filesize