A watershed is the area that drains to a point on a river. By analogy, a windshed is the area that wind connects to a site. Because wind is directional, every site has two:
- Its downwind windshed: where wind carries material released at the site, such as its pollen, seeds, or spores.
- Its upwind windshed: where material arriving at the site comes from.
The two are usually very different. Under prevailing westerlies, a site’s downwind windshed stretches to the east and its upwind windshed to the west, so the places a population can colonize are not the places its immigrants come from.
This article shows how to map windsheds with both of windscape’s
connectivity models, how to trace the routes that connect a site to its
windshed, and how to summarize windsheds for comparison across sites. It
uses model settings such as half_life without explaining
them in depth; the connectivity
models article covers how the models work and how to choose their
settings.
library(windscape)
library(ggplot2)
rose <- windscape_example("wind_rose")
site <- cbind(-105, 40) # north-central Colorado
states <- map_data("state")
borders <- geom_path(data = states, aes(long, lat, group = group),
color = "white", linewidth = 0.15, inherit.aes = FALSE)
us <- coord_quickmap(xlim = c(-120, -90), ylim = c(30, 50), expand = FALSE)
facets <- theme(strip.text = element_text(margin = margin(4, 0, 4, 0)))Least-cost windsheds
A least-cost windshed maps the fastest wind route between a site and
every place in the landscape. least_cost() returns the
travel time along that route, in hours (for a rose built with
trans = 1 from winds in m/s): downwind, from the site to
each cell, and upwind, from each cell to the site.
down <- least_cost(rose, site, direction = "downwind")
up <- least_cost(rose, site, direction = "upwind")
d <- rbind(data.frame(as.data.frame(down, xy = TRUE), windshed = "downwind: hours from the site"),
data.frame(as.data.frame(up, xy = TRUE), windshed = "upwind: hours to the site"))
ggplot(d, aes(x, y)) +
geom_raster(aes(fill = pmax(hours, 10))) + # floor at 10 hours for the log scale
borders +
geom_contour(aes(z = hours), breaks = c(100, 300, 1000), color = "black", linewidth = 0.2) +
annotate("point", site[1], site[2], size = 1.5) +
facet_wrap(~windshed) +
scale_fill_viridis_c(name = "hours", trans = "log10", direction = -1) +
us + theme_void() + facets
Travel times are a measure of accessibility, not a prediction of when
particles arrive: each step’s cost is the time to cross it at the
long-run average wind speed in that direction, including the time the
wind spends blowing other ways. Small values mean strong connectivity.
rate = TRUE returns the inverse, a flow rate, for analyses
where large values should mean strong connectivity, as they do for
random walks.
Given several sites, least_cost() maps the travel time
to or from the nearest one, for example the windshed of a whole
population or species range, represented by a set of occurrence
points:
range_pts <- cbind(c(-112, -111, -110.5, -112.5, -111.5), c(43, 44.5, 43.5, 44, 42.5))
down_range <- least_cost(rose, range_pts, direction = "downwind")
ggplot(as.data.frame(down_range, xy = TRUE), aes(x, y)) +
geom_raster(aes(fill = pmax(hours, 10))) +
borders +
annotate("point", range_pts[, 1], range_pts[, 2], size = 1) +
scale_fill_viridis_c(name = "hours", trans = "log10", direction = -1) +
us + theme_void()
Least-cost paths
least_cost_paths() returns the routes behind those
travel times, as path data that geom_wind_path() draws. By
default it traces paths between the site and a regular grid of about
n points across the domain, which shows the network of
fastest routes:
paths <- rbind(
data.frame(least_cost_paths(rose, site, n = 300, direction = "downwind"), windshed = "downwind"),
data.frame(least_cost_paths(rose, site, n = 300, direction = "upwind"), windshed = "upwind"))
head(paths)
#> trail site to step hours x y windshed
#> 1 1 1 1 0 0.00000 -105.1579 39.84127 downwind
#> 2 1 1 1 1 23.30238 -105.4737 39.84127 downwind
#> 3 1 1 1 2 52.75803 -105.7895 39.84127 downwind
#> 4 1 1 1 3 113.33490 -106.1053 39.84127 downwind
#> 5 1 1 1 4 254.08530 -106.4211 39.84127 downwind
#> 6 1 1 1 5 324.87818 -106.7368 40.15873 downwind
# travel time between each vertex and the site: elapsed time on downwind paths, and time
# remaining on upwind paths
paths$from_site <- ifelse(paths$windshed == "downwind", paths$hours,
ave(paths$hours, paths$windshed, paths$to, FUN = max) - paths$hours)
ggplot(paths, aes(x, y)) +
geom_path(data = states, aes(long, lat, group = group), color = "gray70",
linewidth = 0.15, inherit.aes = FALSE) +
geom_wind_path(aes(color = pmax(from_site, 10)), linewidth = 0.4, arrow = NULL) +
annotate("point", site[1], site[2], color = "red", size = 1.5) +
facet_wrap(~windshed) +
scale_color_viridis_c(name = "hours", trans = "log10", direction = -1, values = c(0, .5, .7, .85, 1)) +
us + theme_void() + facets
Each row is a vertex of a path, ordered in the direction of travel:
downwind paths start at the site, and upwind paths end there.
hours accumulates along each path, and its final value is
the travel time between the pair. Paths move between neighboring cell
centers, in eight directions, so they are built from horizontal,
vertical, and diagonal segments.
The paths from one site form a tree: once two paths meet, they share the rest of their route to or from the site. Heavily shared branches are the main corridors of wind transport. (Where two diagonal steps cross between cell centers, two branches can appear to cross without meeting.)
Paths can also run to or from chosen points, with to.
pairs = "nearest" traces one path for each site, to or from
the point in to with the shortest travel time, and
pairs = "matched" pairs sites and
to row by row; see ?least_cost_paths.
Random walk windsheds
A random walk windshed instead follows particles diffusing through
the wind rose along all routes, in proportion to how much wind flows
along each, and depositing as they go. In stream mode,
random_walk() returns the long-run result of continuous
release from the site. Its deposition layer is the downwind
windshed: the probability density (per km^2) that a particle released at
the site lands in each cell.
down <- random_walk(rose, site, mode = "stream", direction = "downwind", half_life = 48)
names(down)
#> [1] "residence" "deposition"Upwind walks with a finite half-life have two layers that describe the upwind windshed, and they answer different questions:
-
deposition: how likely a particle released in each cell is to be deposited at the site (by default, as a density per km^2 of the site’s cell). This asks, “if particles were released here, would they reach the site?” -
origin: the share of the particles deposited at the site that come from each cell (by default, per km^2). This asks, “of the particles reaching the site, what share came from here?”
origin is deposition weighted by how much
each cell releases, and normalized. By default, every cell releases the
same amount per km^2, so the two maps have the same shape. They differ
when release varies, given as a raster via the source
argument, such as the abundance of a species that produces the
particles: then origin shows where the site’s immigrants
actually come from.
up <- random_walk(rose, site, mode = "stream", direction = "upwind", half_life = 48)
d <- rbind(data.frame(fortify(down)[c("x", "y")], value = fortify(down)$deposition,
windshed = "downwind: where particles released here land"),
data.frame(fortify(up)[c("x", "y")], value = fortify(up)$origin,
windshed = "upwind: where particles landing here come from"))
d$value <- d$value / ave(d$value, d$windshed, FUN = max) # relative to each windshed's peak
ggplot(d, aes(x, y)) +
geom_raster(aes(fill = value)) +
borders +
annotate("point", site[1], site[2], color = "red", size = 1.5) +
facet_wrap(~windshed) +
scale_fill_viridis_c(name = "relative\ndensity", trans = "log10",
limits = c(1e-4, 1), oob = scales::squish) +
us + theme_void() + facets
These windsheds point the same ways as the least-cost ones, but they measure something different. A least-cost map gives every place a travel time, however remote. A random walk map gives the share of the site’s material that actually gets there, which falls off steeply with distance as particles are deposited along the way (note the log scale). The half-life sets the windshed’s reach: short half-lives keep it close to the site, and long ones extend it across the landscape.
Like least_cost(), random_walk() accepts
several sites, releasing one unit from each grid cell that contains a
site. For uneven release, such as from a population whose abundance
varies across its range, give init as a raster of release
amounts on the rose’s grid:
abundance <- rose[[1]] * 0 # a raster on the rose's grid
abundance[terra::cellFromXY(abundance, range_pts)] <- c(5, 1, 2, 1, 3)
range_windshed <- random_walk(rose, abundance, mode = "stream", half_life = 48)Random walk paths
random_walk_paths() is the random walk counterpart to
least_cost_paths(), and returns path data in a similar
layout (trail, path, step,
x, and y), for drawing with
geom_wind_path(). Despite its name, it involves no
randomness. It doesn’t simulate the zigzagging paths of
individual particles. It traces streamlines of the walk’s net flux of
material: the average routes along which material moves. Running it
twice gives identical results.
Without to, random_walk_paths() places the
ends of n paths in proportion to where material is
deposited (or leaves the domain), so each path carries an equal share of
the released material. The density of paths shows where material goes,
and a dot at the end of each path shows where its share lands:
rw_paths <- rbind(
data.frame(random_walk_paths(rose, site, n = 150, half_life = 48), windshed = "downwind"),
data.frame(random_walk_paths(rose, site, n = 150, half_life = 48, direction = "upwind"),
windshed = "upwind"))
# where each path's material lands (downwind) or originates (upwind)
id <- paste(rw_paths$windshed, rw_paths$path)
ends <- rw_paths[ifelse(rw_paths$windshed == "downwind", !duplicated(id, fromLast = TRUE), !duplicated(id)), ]
ggplot(rw_paths, aes(x, y)) +
geom_path(data = states, aes(long, lat, group = group), color = "gray70",
linewidth = 0.15, inherit.aes = FALSE) +
geom_wind_path(alpha = 0.5, linewidth = 0.3, arrow = NULL) +
geom_point(data = ends, size = 0.3, color = "dodgerblue") +
annotate("point", site[1], site[2], color = "red", size = 1.5) +
facet_wrap(~windshed) +
us + theme_void() + facets
Upwind paths start where material originates, placed in proportion to
origin, and end at the site. Because most material is
deposited close to where it is released, most paths are short; a larger
n resolves more long-distance routes.
Comparing the two kinds of paths
The two models answer different questions about the route between two
places. A least-cost path is the single fastest route. A random walk
path is the average route of the material that actually makes the trip,
which spreads across all routes. Given the same destinations with
to, the difference is easy to see:
to <- cbind(c(-94, -92, -97, -100, -96), c(46, 38, 33, 47, 41))
both <- rbind(
data.frame(least_cost_paths(rose, site, to)[c("trail", "step", "x", "y")], model = "least-cost"),
data.frame(random_walk_paths(rose, site, to = to, half_life = 48)[c("trail", "step", "x", "y")],
model = "random walk"))
ggplot(both, aes(x, y)) +
geom_path(data = states, aes(long, lat, group = group), color = "gray70",
linewidth = 0.15, inherit.aes = FALSE) +
geom_wind_path(aes(color = model, group = interaction(model, trail)), linewidth = 0.6) +
annotate("point", site[1], site[2], size = 1.5) +
annotate("point", to[, 1], to[, 2], size = 1, shape = 1) +
scale_color_manual(values = c("darkorange", "dodgerblue")) +
us + theme_void()
Least-cost paths are built from grid steps and can zigzag where the fastest route lies between two neighbor directions. Random walk paths are smooth, and can depart widely from the fastest route: the material that reaches the southernmost point travels mostly by way of New Mexico and Texas, swinging around with the winds there, rather than taking the most direct fast route.
Paths to chosen points no longer carry equal shares of material, so
with to, the density of random walk paths has no meaning.
Points the material doesn’t reach can’t be traced and are dropped with a
warning.
Flux
Paths sample the flow of material at a few places. To map it
everywhere, random_walk() returns the net flux with
flux = TRUE: the direction and rate at which material moves
through each cell. It’s a wind_field, so the wind field
layers draw it. Flux spans orders of magnitude, from near the site to
the windshed’s fringes, so it’s clearer to show its magnitude as line
width (here on a square-root scale) and draw all trails the same length.
Arrowheads don’t shrink with line width, so they’re left off here;
material flows outward from the site.
down <- random_walk(rose, site, mode = "stream", half_life = 48, flux = TRUE)
ggplot(down$flux, aes(x, y)) +
geom_path(data = states, aes(long, lat, group = group), color = "gray70",
linewidth = 0.15, inherit.aes = FALSE) +
geom_wind_trail(aes(linewidth = after_stat(speed)), fixed_length = TRUE, res = 25,
arrow = NULL, lineend = "round") +
annotate("point", site[1], site[2], color = "red", size = 1.5) +
scale_linewidth(trans = "sqrt", range = c(0, 2), guide = "none") +
us + theme_void()
Flux shows the transport that produces a windshed, which its shape
alone doesn’t: across the flanks of a plume, for example, material moves
mostly downwind rather than sideways toward lower density. For an upwind
walk (which requires a finite half_life), flux is the
transport of the material that ends up at the site, showing the routes
by which it arrives.
Comparing windsheds across sites
To compare windsheds among many sites, ws_summarize()
reduces a windshed to summary statistics, including the distance and
bearing from the site to its centroid, and the windshed’s mean bearing
and spread of bearings. It expects large values to mean strong
connectivity, so use rate = TRUE for least-cost
windsheds:
ws_summarize(least_cost(rose, site, rate = TRUE), site)[c("centroid_distance", "centroid_bearing")]
#> centroid_distance centroid_bearing
#> 152.48696 82.39936
ws_summarize(down$deposition, site)[c("centroid_distance", "centroid_bearing")]
#> centroid_distance centroid_bearing
#> 167.76064 86.73836Both windsheds center east of the site. See
?ws_summarize for all the statistics.
Plotting tips
The maps above convert windsheds to data frames:
as.data.frame(x, xy = TRUE) for least-cost rasters, and
fortify() for random walk results. The
geom_spatraster() layer from the tidyterra package
plots any SpatRaster directly, including a single windshed
layer:
ggplot() +
tidyterra::geom_spatraster(data = least_cost(rose, site)) +
scale_fill_viridis_c(trans = "log10")Windsheds span orders of magnitude, so log color scales usually work best. Least-cost travel times are zero at the site and random walk densities approach zero far from it, so floor or squish values before log-transforming, as in the maps above.
