
Delineate unconfined valleys using the Valley Confinement Algorithm
Source:R/fl_valley_confine.R
fl_valley_confine.RdOrchestrates the full VCA pipeline: slope thresholding, cost-distance analysis, flood surface modelling, and morphological cleanup to identify unconfined valley bottoms.
Usage
fl_valley_confine(
dem,
streams,
area_field,
slope = NULL,
slope_threshold = 9,
max_width = 2000,
cost_threshold = 2500,
flood_factor = 6,
precip = NULL,
waterbodies = NULL,
channel_buffer = NULL,
size_threshold = 5000,
hole_threshold = 2500,
field = NULL
)Arguments
- dem
A
SpatRasterof elevation.- streams
An
sflinestring object or aSpatRasterof rasterized streams. Ifsf, it is rasterized usingarea_field. If aSpatRaster, its cell values are used as-is and must already be upstream contributing area in hectares.- area_field
Character. Column of
streamsholding upstream contributing area in hectares, rasterized onto the DEM grid byfl_stream_rasterize(). Required whenstreamsissf— there is no default. The rasterized values become the drainage-area term of the bankfull regression infl_flood_surface(), which accepts any positive numeric column without complaint: channel width in place of area returns a smaller floodplain with no error and no warning. Not used whenstreamsis already aSpatRaster. That branch cannot inspect the values it is handed; it warns only when the layer's name gives it away, so a raster burned from any other wrong column carries the same defect one call earlier, undetected.- slope
A
SpatRasterof percent slope. IfNULL, derived fromdem.- slope_threshold
Numeric. Maximum percent slope for valley floor. Default
9.- max_width
Numeric. Maximum valley width in map units (metres). Default
2000.- cost_threshold
Numeric. Maximum accumulated cost distance. Default
2500.- flood_factor
Numeric. Multiplier on bankfull depth. Default
6.- precip
A
SpatRasteror numeric scalar of mean annual precipitation in millimetres, converted to cm/yr internally for the bankfull regression. DefaultNULL, which drops the precipitation term.- waterbodies
An
sfpolygon object of lakes and/or wetlands, orNULL(default). Waterbody polygons are rasterized onto the valley grid and added to the output after morphological cleanup. No buffer is applied — a lake or wetland in the valley is part of the flood system as-is. No spatial filtering is applied — all polygons are rasterized. Pre-filter to valley-bottom features before calling if headwater waterbodies are not wanted.- channel_buffer
Logical. Buffer streams by their
channel_widthattribute and add to the valley output. DefaultTRUEwhenstreamsis ansfobject with achannel_widthcolumn,FALSEotherwise. The stream channel is floodplain but can be sub-pixel at coarse DEM resolution.- size_threshold
Numeric. Minimum valley patch area (m²). Default
5000.- hole_threshold
Numeric. Maximum hole area to fill (m²). Default
2500.- field
Deprecated. The former name of
area_field, whose"channel_width"default was wrong for the flood model (#47). Supplying it warns and forwards toarea_field; removal is tracked in flooded#53.
Value
A SpatRaster with binary values: 1 = unconfined valley, 0 =
confined / hillslope. A cell is NA wherever dem is NA, unless the
channel buffer or a waterbody covers it, in which case it is 1. Every
cell with a DEM value is 0 or 1. That includes a cell that a gap cuts
off from every stream, which has no cost distance, and a cell whose slope
is NA: neither can pass the criterion it cannot be tested on, but the
morphological cleanup can still mark one 1 when it lies within a few
cells of valley floor.
Details
The algorithm combines four criteria via intersection (AND):
Slope mask — cells with slope <=
slope_thresholdDistance mask — cells within
max_width / 2of a streamCost distance mask — cells with accumulated cost <
cost_thresholdFlood mask — cells identified as flooded by bankfull regression
The combined mask then undergoes morphological cleanup:
Closing filter (3x3) to bridge small gaps
Fill small holes (<
hole_threshold)Remove small patches (<
size_threshold)Majority filter (3x3) to smooth edges
The cleaned result is then set to NA wherever dem is NA, so a coverage
gap (patchy lidar, a DEM clipped to a watershed) reads as unmeasured rather
than as measured ground. The gap still enters the slope, cost-distance and
flood steps, so valid cells around it, not only at its edge, can differ from
a run on a complete DEM.
After cleanup, optional features are added via logical OR. They do not depend
on the DEM, so they apply inside NA gaps too:
Channel buffer — streams buffered by
channel_width(DEM correction)Waterbodies — user-supplied lake/wetland polygons rasterized as-is
Adapted from the USDA Valley Confinement Algorithm Toolbox (BlueGeo implementation by Devin Cairns, MIT license) and bcfishpass lateral habitat assembly (Simon Norris, Apache 2.0).
Performance
On the Parsnip River watershed group's cached inputs (a 3,749 x 3,761 cell
grid at 30 m, 8,436 stream segments and 664 waterbodies, the inputs before
#87 rebuilt them from link's tables, flood_factor = 4),
one call took 83 to 122 s of wall time on an Apple M1 Max (10 cores) with
flooded 0.8.1 and terra 1.9.50, with other work on the machine (1-minute
load average 6 to 17 before each run). Three runs each at terra::terraOptions(threads = 1)
and threads = 8 overlapped, so any effect of terra's threads option
(default 16) could not be separated from that load (#81).
See also
fl_stream_rasterize() for how area_field is burned onto the
grid, and fl_flood_surface() for the regression that consumes it.
fl_mask(), fl_cost_distance(), fl_flood_model(), fl_patch_rm(),
fl_valley_poly()
Examples
dem <- terra::rast(system.file("testdata/dem.tif", package = "flooded"))
streams <- sf::st_read(
system.file("testdata/streams.gpkg", package = "flooded"),
quiet = TRUE
)
precip_r <- fl_stream_rasterize(streams, dem, field = "map_upstream")
# Basic VCA (channel buffer auto-detected from streams$channel_width)
valleys <- fl_valley_confine(
dem, streams,
area_field = "upstream_area_ha",
precip = precip_r
)
terra::plot(valleys, col = c("grey90", "darkgreen"), main = "Unconfined valleys")
# With waterbodies — fills lake/wetland donut holes
waterbodies <- sf::st_read(
system.file("testdata/waterbodies.gpkg", package = "flooded"),
quiet = TRUE
)
valleys_wb <- fl_valley_confine(
dem, streams,
area_field = "upstream_area_ha",
precip = precip_r,
waterbodies = waterbodies
)
terra::plot(valleys_wb, col = c("grey90", "darkgreen"), main = "With waterbodies")