Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2f8315f
intial commit for catchment boundary tool
RileyMcDermott-NOAA Mar 11, 2024
50aa51f
descriptive variable names
RileyMcDermott-NOAA Mar 26, 2024
1faa37b
Multiprocess and optimized code
RileyMcDermott-NOAA May 7, 2024
f083cd6
formatting fixes
RileyMcDermott-NOAA May 7, 2024
b81bfaf
Updated changelog
RileyMcDermott-NOAA May 7, 2024
017858f
dev-catchment-boundary-tool
RileyMcDermott-NOAA May 14, 2024
a068924
Merge branch 'dev' into dev-catchment-boundary-tool
RileyMcDermott-NOAA May 14, 2024
b38fab6
Updated function documentation
RileyMcDermott-NOAA May 14, 2024
007a48c
Updated function documentation
RileyMcDermott-NOAA May 21, 2024
c2f1539
update changelog
CarsonPruitt-NOAA May 21, 2024
6829131
merge with dev
CarsonPruitt-NOAA May 21, 2024
a211765
merge with dev
CarsonPruitt-NOAA May 21, 2024
269ce42
code linting and formatting
CarsonPruitt-NOAA May 21, 2024
1d703c4
Merge branch 'dev' into dev-catchment-boundary-tool
Jul 19, 2024
d0bb959
Merge branch 'dev' into dev-catchment-boundary-tool
RileyMcDermott-NOAA Aug 12, 2024
f986576
Merge branch 'dev-catchment-boundary-tool' into dev-catchment-boundar…
RileyMcDermott-NOAA Aug 12, 2024
f29b5e6
merge with dev
RileyMcDermott-NOAA Aug 19, 2024
c8c43f7
Update process and link catchment boundary lines to HydroID and featu…
RileyMcDermott-NOAA Aug 19, 2024
81ca5ec
Removed loops added minumum length arguement and updated comments.
RileyMcDermott-NOAA Aug 26, 2024
5be793a
Updated inundate catchment boundary to include minumum error length a…
RileyMcDermott-NOAA Aug 29, 2024
dc8f2f6
Linting
CarsonPruitt-NOAA Aug 30, 2024
602306b
merge with dev
CarsonPruitt-NOAA Aug 30, 2024
59cbe9d
fix files not changed
CarsonPruitt-NOAA Aug 30, 2024
a99d35b
Quick changelog fix
Sep 13, 2024
498905e
Merge https://github.com/NOAA-OWP/inundation-mapping into dev-catchme…
Sep 13, 2024
01acfa9
merge in dev
Sep 13, 2024
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
11 changes: 11 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
All notable changes to this project will be documented in this file.
We follow the [Semantic Versioning 2.0.0](http://semver.org/) format.

## v4.5.x.x - 2024-05-21 - [PR#1149](https://github.com/NOAA-OWP/inundation-mapping/pull/1149)

This PR adds scripts that can identify areas within produced inundation rasters where glasswalling of inundation occurs due to catchment boundaries, know as catchment boundary issues.

### Additions
- `tools/identify_catchment_boundary.py`: Identifies where catchment boundaries are glasswalling inundation extent.

- `tools/inundate_catchment_boundary.py`: Produces inundation for given HUC and identifies catchment boundary issues in produced FIM.

<br/><br/>


## v4.5.2.1 - 2024-05-21 - [PR#1172](https://github.com/NOAA-OWP/inundation-mapping/pull/1172)

Expand Down
171 changes: 171 additions & 0 deletions tools/identify_catchment_boundary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import argparse
import os
import sys
import traceback
from concurrent import futures
from concurrent.futures import ProcessPoolExecutor, as_completed, wait
from timeit import default_timer as timer

import geopandas as gpd
import numpy as np
import pandas as pd
import rasterio
from rasterio import features
from shapely.geometry import shape


def catchment_boundary_errors(hydrofabric_dir, hucs, inundation_raster, output, number_of_jobs):
"""
This function compares output inundation raster extent to catchment extents to identify catchment boundary
issues. The output of this function is a geopackage of linefiles that identifys areas of inundation with catchment
boundary issues present.

Args:
hydrofabric_dir (str): Path to hydrofabric directory where FIM outputs were written by
fim_pipeline.
hucs (str): The HUC for which to check for catchment boundary issues.
inundation_raster (str): Full path to output inundation raster
(encoded by positive and negative HydroIDs).
output (str): Path to output location for catchment boundary geopackage.
number_of_jobs (int): Number of parallel jobs to run.
"""

# Validation
total_cpus_available = os.cpu_count() - 1
if number_of_jobs > total_cpus_available:
raise ValueError(
f'The number of jobs provided: {number_of_jobs} ,'
' exceeds your machine\'s available CPU count minus one.'
' Please lower the number of jobs'
' value accordingly.'
)
intersect_lines_final = pd.DataFrame()

with ProcessPoolExecutor(max_workers=number_of_jobs) as executor:
executor_dict = {}

for huc in hucs:
huc_feature_args = {
'hydrofabric_dir': hydrofabric_dir,
'huc': huc,
'inundation_raster': inundation_raster,
'output': output,
}

try:
future = executor.submit(identify_per_huc, **huc_feature_args)
executor_dict[future] = huc
except Exception as ex:
summary = traceback.StackSummary.extract(traceback.walk_stack(None))
print(f"*** {ex}")
print(''.join(summary.format()))

sys.exit(1)

for future_result in futures.as_completed(executor_dict):
if future_result is not None:
boundary_line_df = future_result.result()
if boundary_line_df is None:
print('Process failed.')
else:
intersect_lines_final = pd.concat([intersect_lines_final, boundary_line_df])
# Export final file
intersect_lines_final.to_file(output, index=False)


def identify_per_huc(hydrofabric_dir, huc, inundation_raster, output):
print(f'Processing HUC:{huc} now.')
# Get branch names for input HUC
dirs = [x[1] for x in os.walk(f"{hydrofabric_dir}/{huc}/branches")][0]

# merge all catchment geopackages into one file
catchments = gpd.GeoDataFrame()
for d in dirs:
catch = gpd.read_file(
f"{hydrofabric_dir}/{huc}/branches/{d}/gw_catchments_reaches_filtered_addedAttributes_crosswalked_{d}.gpkg"
)
catchments = pd.concat([catchments, catch], ignore_index=True)

# Vectorize inundation
with rasterio.open(inundation_raster) as src:
affine = src.transform
band = src.read(1)
band[np.where(band <= 0)] = 0
band[np.where(band > 0)] = 1
results = (
{'properties': {'raster_val': v}, 'geometry': s}
for i, (s, v) in enumerate(features.shapes(band, mask=None, transform=affine))
)

# Save features to geodataframe and select inundated pixels
geoms = list(results)
inund_poly = gpd.GeoDataFrame.from_features(geoms)
inund_poly = inund_poly.loc[inund_poly['raster_val'] == 1.0]

# Get boundary lines for catchments and inundation
catchment_boundary = catchments.boundary
inundation_boundary = inund_poly.boundary
inundation_boundary = inundation_boundary.set_crs(catchment_boundary.crs)

# Save boundary lines to geodataframe
inundation_boundary_df = gpd.GeoDataFrame(geometry=inundation_boundary)
catchment_boundary_df = gpd.GeoDataFrame(geometry=catchment_boundary)

# Dissolve geomtries into one multilinestring
catchment_boundary_dissolve = catchment_boundary_df.dissolve()
inundation_boundary_dissolve = inundation_boundary_df.dissolve()

# Find intersection of inundation and catchments
intersect = gpd.GeoDataFrame()
intersect = gpd.GeoDataFrame(
geometry=inundation_boundary_dissolve.intersection(catchment_boundary_dissolve)
)

intersect_exp = intersect.explode(ignore_index=True)
intersect_lines = intersect_exp.loc[intersect_exp.geom_type == 'LineString']
num_catchment_boundary_lines = len(intersect_lines)

print(
f'Finished processing huc: {huc}. Number of boundary line issues identified: {num_catchment_boundary_lines}.'
)

return intersect_lines


if __name__ == "__main__":
# Parse arguments
parser = argparse.ArgumentParser(
description="Helpful utility to produce mosaicked inundation extents (raster and poly) and depths."
)
parser.add_argument(
"-y",
"--hydrofabric_dir",
help="Directory path to FIM hydrofabric by processing unit.",
required=True,
type=str,
)
parser.add_argument(
"-u", "--hucs", help="List of HUCs to run", required=True, default="", type=str, nargs="+"
)
parser.add_argument(
"-i", "--inundation-raster", help="Inundation raster output.", required=True, default=None, type=str
)
parser.add_argument(
"-o", "--output", help="Output geopackage location.", required=True, default=None, type=str
)
parser.add_argument(
'-j',
'--number_of_jobs',
help='OPTIONAL: number of cores/processes (default=4). This is a memory intensive '
'script, and the multiprocessing will crash if too many CPUs are used. It is recommended to provide '
'half the amount of available CPUs.',
type=int,
required=False,
default=4,
)

start = timer()

catchment_boundary_errors(**vars(parser.parse_args()))

print(f"Completed in {round((timer() - start)/60, 2)} minutes.")
Loading