Because wind disperses particles ranging from pollen, seeds, and spores to insects, pathogens, and pollutants, wind patterns shape many aspects of spatial ecology. But the variation in wind strength and direction over space and time makes it challenging to study wind’s role in landscape connectivity. The windscape package provides a toolset for modeling the effects of time-integrated wind regimes in spatial ecology, landscape genetics, and related fields.
While windscape can be used to map air flow patterns at a single moment, its core connectivity models summarize long time series of wind data into wind regimes, describing the expected connectivity averaged over many dispersal events. It is thus designed for analyzing processes that accumulate over time, such as gene flow, colonization, and range expansion. The package offers the following functionality:
| Task | Description | Functions |
|---|---|---|
| Get wind data | Download and import wind data rasters or pre-built wind roses | download_wind_data(),
download_wind_rose(), download_land_mask(),
wind_series(), windscape_example() |
| Model wind regimes | Summarize wind time series into time-integrated neighbor connectivity models | wind_rose(),
combine_roses(), weight_conductance(),
downscale() |
| Map windsheds | Model a site’s upwind catchment area and downwind deposition shadow using least cost path or random walk approaches | least_cost(),
least_cost_paths(), random_walk(),
random_walk_paths(), ws_summarize() |
| Compute pairwise connectivity | Model directional connectivity among sets of sites | pairwise_least_cost(),
pairwise_random_walk(),
check_cell_distance() |
| Test hypotheses | Run statistical significance tests about spatial relationships between wind and ecological outcomes | mantel_test(),
pairwise_ratios(), pairwise_means(),
point_distance() |
| Trace airflow | Trace the paths of airborne particles through an instantaneous wind field | wind_trails() |
| Visualize | Map spatial wind patterns in ggplot2, including instantaneous wind fields, time-integrated wind regimes, and windsheds | geom_wind_rose(),
geom_wind_arrow(), geom_wind_trail(),
geom_wind_path(), scale_fill_bearing() |
# You can install the package from CRAN:
install.packages("windscape")
# Or get the development version from GitHub:
remotes::install_github("matthewkling/windscape")download_wind_data() downloads hourly wind data from
NCAR’s Geoscience Data Exchange, with no account needed. Datasets
include ERA5 (1940 to present), CFSR (1979-2010), and CFSv2 (2011 to
present). Data are clipped to your region on the server and saved as one
file per month.
library(windscape)
# a decade of 10 m altitude ERA5 winds for the western and central US, every 3rd hour
files <- download_wind_data("era5", xlim = c(-120, -90), ylim = c(30, 50),
years = 2011:2020, hours = seq(0, 21, 3), dir = "~/wind_data")A wind_field is a pair of rasters representing the wind
conditions across a landscape at a single moment. Most
windscape modeling analyses involve summarizing many
wind fields, but we can also visualize an individual field using
geom_wind_arrow() to draw wind vectors, or
geom_wind_trail() to draw trails that follow the airflow,
here for Hurricane Katrina on August 28, 2005.
(wind_trails() returns the same trails as data.)
library(windscape)
library(ggplot2)
katrina <- windscape_example("wind_field")
world <- map_data("world")
ggplot(katrina, aes(x, y)) +
geom_raster(aes(fill = speed)) +
geom_path(data = world, aes(long, lat, group = group), color = "white", linewidth = 0.2) +
stat_wind_trail(color = "white", hours = 6, steps = 100) +
scale_fill_gradientn(name = "wind speed\n(m/s)", colors = c("black", "orangered")) +
coord_quickmap(xlim = c(-99, -78), ylim = c(17, 35), expand = FALSE) +
theme_void()
A wind rose summarizes a time series of wind fields
into a model of the wind regime: for each grid cell, the average
conductance of wind toward each of its eight neighbors.
wind_rose() builds one from a long series in chunks, so
long records needn’t fit in memory. The trans argument sets
how wind speed translates into conductance so the rose reflects the
winds that matter for dispersal, for example to model propagules that
are only released in strong winds.
rose <- wind_rose(wind_series(files), trans = 1)The examples below use a small wind_rose object that
ships with the package, built from the same region as the data
downloaded above. geom_wind_rose() maps it as a field of
glyphs, each showing the distribution of flow directions in a block of
cells, colored by the direction of net flow:
rose <- windscape_example("wind_rose")
states <- map_data("state")
ggplot(rose, aes(x, y)) +
geom_raster(aes(alpha = speed), fill = "black") +
geom_path(data = states, aes(long, lat, group = group), color = "black", linewidth = 0.2) +
geom_wind_rose(res = 12, scale = 1.5) +
coord_quickmap(xlim = ext(rose)[1:2], ylim = ext(rose)[3:4], expand = FALSE) +
theme_void() +
labs(alpha = "mean wind\nspeed (km/h)")
By analogy to watersheds, “windsheds” are the upwind catchment area
where particles arriving at a site originate, or the downwind deposition
area where particles released from the site end up. These two surfaces
represent how wind connects a site to the surrounding landscape, and
typically differ strongly from each other due to the directionality of
wind patterns. windscape offers two complementary ways to model this
connectivity, both of which use a wind_rose as input.
Least-cost path models find the fastest route between places, giving
wind travel times in hours of effective wind transport along the best
route under the long-run wind regime. least_cost() maps
travel time from a site, and least_cost_paths() traces a
set of individual routes. Here we use them together to model
downwind connectivity from a site in the center of the
landscape.
site <- cbind(-105, 40)
hours <- least_cost(rose, site, direction = "downwind")
paths <- least_cost_paths(rose, site, direction = "downwind", n = 500)
ggplot(as.data.frame(hours, xy = TRUE), aes(x, y)) +
geom_raster(aes(fill = pmax(hours, 10))) + # floor at 10 hours for the log scale
geom_path(data = states, aes(long, lat, group = group), color = "black", linewidth = 0.2) +
geom_wind_path(data = paths, alpha = .5, color = "white") +
annotate("point", site[1], site[2], color = "black", size = 3) +
scale_fill_gradientn(name = "hours", trans = "log10", values = c(0, .5, .7, .85, 1),
colors = c("cyan", "dodgerblue", "purple", "red", "orange")) +
coord_quickmap(xlim = c(-120, -90), ylim = c(30, 50), expand = FALSE) +
theme_void()
Random walk models instead simulate particles diffusing through the
wind regime, optionally with deposition at a rate set by a half-life.
They capture dispersal along all routes, not just the fastest one. Here
we use random_walk() to compare the downwind and upwind
windsheds of the same site:
down <- random_walk(rose, site, mode = "stream", direction = "downwind", half_life = 100)
up <- random_walk(rose, site, mode = "stream", direction = "upwind", half_life = 100)
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)) +
geom_path(data = states, aes(long, lat, group = group), color = "white", linewidth = 0.15) +
annotate("point", site[1], site[2], color = "red", size = 2) +
facet_wrap(~windshed) +
scale_fill_viridis_c(name = "relative\ndensity", trans = "log10",
limits = c(1e-4, 1), oob = scales::squish) +
coord_quickmap(xlim = c(-120, -90), ylim = c(30, 50), expand = FALSE) +
theme_void() +
theme(strip.text = element_text(margin = margin(4, 0, 4, 0)))
For comparing windsheds among many sites, ws_summarize()
computes a windshed’s summary statistics such as its size, its centroid,
and how concentrated it is in a few compass directions.
Many studies require pairwise connectivity estimates among a set of
sites, such as sampled populations. pairwise_least_cost()
and pairwise_random_walk() return matrices of wind
connectivity between every pair of sites. Wind connectivity is
directional, so these matrices are asymmetric: element
[i, j] describes flow from site i to site
j. The least-cost model uses each site’s exact location,
even for sites within the same grid cell, while the random walk model
treats each site as the grid cell it falls in;
check_cell_distance() reports how much that distorts
distances among closely spaced sites.
sites <- cbind(lon = c(-110, -105, -100, -95), lat = c(40, 42, 38, 44))
pairwise_least_cost(rose, sites) |> round() # travel time, in hours## [,1] [,2] [,3] [,4]
## [1,] 0 121 379 568
## [2,] 824 0 277 459
## [3,] 1081 362 0 309
## [4,] 1221 538 503 0
pairwise_random_walk(rose, sites, half_life = 100) |> signif(2) # deposition density## [,1] [,2] [,3] [,4]
## [1,] 3.6e-05 1.3e-06 2.4e-08 6.6e-08
## [2,] 1.3e-14 1.4e-05 3.1e-08 1.5e-07
## [3,] 1.5e-15 7.5e-10 1.9e-05 3.1e-07
## [4,] 4.2e-18 7.1e-11 1.5e-10 2.3e-05
These matrices can be compared with ecological data, such as genetic
differentiation or gene flow, to test whether wind shapes ecological
patterns. pairwise_means() and
pairwise_ratios() derive matrices for testing hypotheses
about the strength and the directionality of wind connectivity, and
mantel_test() tests their relationships with other pairwise
data. The package includes birch, a landscape genetic data
set for silver birch, for trying these out.
The package website includes a more detailed getting started vignette, and articles on each part of the workflow in more depth: