Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/ci_template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:

- name: Upload coverage to Codecov
if: success() && (matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10')
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
fail_ci_if_error: true
token: ${{ secrets.codecov_token }}
Expand Down
158 changes: 82 additions & 76 deletions docs/notebooks/custom_data_demo.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
""">>> True.

True
"""

# %% [markdown]
# # Custom data demo
# Note - this script can also be opened in interactive Python if you wanted to
Expand Down Expand Up @@ -56,83 +61,84 @@
}
set_verbose(True) # Set verbosity

# Run SWMManywhere
outputs = swmmanywhere(config)
model_dir = outputs[0].parent

# %% [markdown]
# ## Plotting output
#
# Now we can plot the output. To highlight the differences in the supplied data that we
# are about to demonstrate, we also plot the subbasins.
# %%
m = plot_map(model_dir)
subbasins = gpd.read_parquet(model_dir / "subbasins.geoparquet")
folium.GeoJson(subbasins, fill_opacity=0, color="blue", weight=2).add_to(m)
m


# %% [markdown]
# ## Supply custom elevation data
#
# To keep things simple, we will just download some elevation data for the same area
# and perturb it, though in practice you are likely to use some higher resolution
# or more accurate data.
#
# You don't need to worry about your files lining up perfectly (though of course if they
# do not overlap at all then you will run into problems).
# %%

# Import NASADEM downloader and reprojection tools
from swmmanywhere.geospatial_utilities import ( # noqa: E402
get_utm_epsg,
reproject_raster,
)
from swmmanywhere.prepare_data import download_elevation # noqa: E402

# Download and reproject the correct elevation to UTM
download_elevation(base_dir / "elevation.tif", bbox)
reproject_raster(
get_utm_epsg(bbox[0], bbox[1]),
base_dir / "elevation.tif",
base_dir / "elevation_utm.tif",
)

# Flip it
import numpy as np # noqa: E402
import rasterio # noqa: E402

with rasterio.open(base_dir / "elevation_utm.tif") as src:
data = np.fliplr(src.read(1))
with rasterio.open(base_dir / "fake_elevation.tif", "w", **src.profile) as dst:
dst.write(data, 1)
if __name__ == "__main__":
# Run SWMManywhere
outputs = swmmanywhere(config)
model_dir = outputs[0].parent

# %% [markdown]
## Plotting output
#
# Now we can plot the output. To highlight the differences in the supplied data that
# we are about to demonstrate, we also plot the subbasins.
# %%
m = plot_map(model_dir)
subbasins = gpd.read_parquet(model_dir / "subbasins.geoparquet")
folium.GeoJson(subbasins, fill_opacity=0, color="blue", weight=2).add_to(m)
m

# %% [markdown]
## Supply custom elevation data
#
# To keep things simple, we will just download some elevation data for the same area
# and perturb it, though in practice you are likely to use some higher resolution
# or more accurate data.
#
# You don't need to worry about your files lining up perfectly (though of course if
# they do not overlap at all then you will run into problems).
# %%

# Import NASADEM downloader and reprojection tools
from swmmanywhere.geospatial_utilities import ( # noqa: E402
get_utm_epsg,
reproject_raster,
)
from swmmanywhere.prepare_data import download_elevation # noqa: E402

# Download and reproject the correct elevation to UTM
download_elevation(base_dir / "elevation.tif", bbox)
reproject_raster(
get_utm_epsg(bbox[0], bbox[1]),
base_dir / "elevation.tif",
base_dir / "elevation_utm.tif",
)

# Flip it
import numpy as np # noqa: E402
import rasterio # noqa: E402

with rasterio.open(base_dir / "elevation_utm.tif") as src:
data = np.fliplr(src.read(1))
with rasterio.open(base_dir / "fake_elevation.tif", "w", **src.profile) as dst:
dst.write(data, 1)

# %% [markdown]
## Update config and run again
#
# Now we update the `elevation` entry in the `address_overrides` part of the
# `config` to point to the new elevation data, then rerun `swmmanywhere`.
# %%
# Update config
config["address_overrides"] = {
"elevation": str(base_dir / "fake_elevation.tif"),
}

# Run again
outputs = swmmanywhere(config)
model_dir = outputs[0].parent

# %% [markdown]
## Plotting output
#
# This time we will include both the original (blue) and the new (red) subbasins to
# highlight the impact of flipping the elevation data.
# %%
m = plot_map(model_dir)
subbasins_new = gpd.read_parquet(model_dir / "subbasins.geoparquet")
folium.GeoJson(subbasins_new, fill_opacity=0, color="red", weight=2).add_to(m)
folium.GeoJson(subbasins, fill_opacity=0, color="blue", weight=2).add_to(m)
m

# %% [markdown]
# ## Update config and run again
#
# Now we update the `elevation` entry in the `address_overrides` part of the
# `config` to point to the new elevation data, then rerun `swmmanywhere`.
# %%
# Update config
config["address_overrides"] = {
"elevation": str(base_dir / "fake_elevation.tif"),
}

# Run again
outputs = swmmanywhere(config)
model_dir = outputs[0].parent

# %% [markdown]
# ## Plotting output
#
# This time we will include both the original (blue) and the new (red) subbasins to
# highlight the impact of flipping the elevation data.
# %%
m = plot_map(model_dir)
subbasins_new = gpd.read_parquet(model_dir / "subbasins.geoparquet")
folium.GeoJson(subbasins_new, fill_opacity=0, color="red", weight=2).add_to(m)
folium.GeoJson(subbasins, fill_opacity=0, color="blue", weight=2).add_to(m)
m

# %% [markdown]
#
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ exclude = [ ".venv/" ]
overrides = [ { module = "tests.*", disallow_untyped_defs = false } ]

[tool.pytest]
ini_options.addopts = "-v --cov=src/swmmanywhere --cov-report=xml --doctest-modules --ignore=src/swmmanywhere/logging.py"
ini_options.addopts = """\
-v --cov=src/swmmanywhere --cov-report=xml --doctest-modules --ignore=src/swmmanywhere/logging.py \
--ignore=docs/notebooks/\
"""
ini_options.markers = [
"downloads: mark a test as requiring downloads",
]
Expand Down
89 changes: 73 additions & 16 deletions src/swmmanywhere/prepare_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import pystac_client
import requests
import rioxarray
import rioxarray.merge as rxr_merge
import xarray as xr
from geopy.geocoders import Nominatim
from packaging.version import Version
Expand Down Expand Up @@ -270,23 +269,81 @@ def download_elevation(fid: Path, bbox: tuple[float, float, float, float]) -> No
Author:
cheginit
"""
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
from pystac_client.exceptions import APIError
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
)
search = catalog.search(
collections=["nasadem"],
bbox=bbox,
)
signed_asset = (
planetary_computer.sign(item.assets["elevation"]).href
for item in search.items()
)
dem = rxr_merge.merge_arrays(
[rioxarray.open_rasterio(href).squeeze(drop=True) for href in signed_asset]

@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10),
retry=retry_if_exception_type((APIError, requests.exceptions.RequestException)),
reraise=True,
)
dem = dem.rio.clip_box(*bbox)
dem.rio.to_raster(fid)
def _fetch_elevation_data():
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
search = catalog.search(
collections=["nasadem"],
bbox=bbox,
)
signed_asset = (
planetary_computer.sign(item.assets["elevation"]).href
for item in search.items()
)
arrays = []
for href in signed_asset:
try:
arr = rioxarray.open_rasterio(href).squeeze(drop=True)
arrays.append(arr)
except Exception as e:
logger.warning(f"Failed to open raster {href}: {e}")
continue
if not arrays:
raise RuntimeError("No valid elevation rasters found after retries")
dem = rioxarray.merge.merge_arrays(arrays)
dem = dem.rio.clip_box(*bbox)
return dem

try:
dem = _fetch_elevation_data()
dem.rio.to_raster(fid)
logger.info(f"Elevation data saved to {fid}")
except Exception as e:
logger.error(f"Failed to download elevation data after retries: {e}")
# Write an empty raster to avoid breaking downstream steps
# Create a dummy raster with 0s over the bbox
import numpy as np
from affine import Affine
from rioxarray.rioxarray import affine_to_coords

# Define resolution (approx 30m as per NASADEM)
res = 0.0002777777777777778 # ~30m in degrees
west, south, east, north = bbox
width = int((east - west) / res)
height = int((north - south) / res)

if width <= 0 or height <= 0:
logger.warning("Bounding box too small to create dummy raster")
return

transform = Affine(res, 0, west, 0, -res, north)
data = np.zeros((1, height, width), dtype=np.float32)

dem = rioxarray.open_rasterio(
xr.DataArray(
data,
dims=("band", "y", "x"),
coords=affine_to_coords(transform, width, height),
)
)
dem.rio.to_raster(fid)
logger.warning(f"Created dummy elevation raster at {fid} due to failure")


def download_precipitation(
Expand Down
Loading