Examples

Forest connectivity in central Maryland

Land cover datasets are commonly used to parameterize resistance for connectivity modeling. This example uses the National Land Cover Dataset for the United States to model forest connectivity in central Maryland. Each value in the categorical land cover dataset is assigned a resistance score. We can have Omniscape.jl assign these values internally by providing a reclassification table (see Resistance Reclassification).

First, install the necessary packages and import them:

using Pkg; Pkg.add(["Omniscape", "Rasters", "Plots"])
using Omniscape, Rasters, Plots

Next, download the landcover data we'll use in this example, and plot it:

url_base = "https://raw.githubusercontent.com/Circuitscape/datasets/main/"
# Download the NLCD tile used to create the resistance surface and load it
download(string(url_base, "data/nlcd_2016_frederick_md.tif"),
         "nlcd_2016_frederick_md.tif")

# Plot the landcover data
values = [11, 21, 22, 23, 24, 31, 41, 42, 43, 52, 71, 81, 82, 90, 95]
palette = ["#476BA0", "#DDC9C9", "#D89382", "#ED0000", "#AA0000",
           "#b2b2b2", "#68AA63", "#1C6330", "#B5C98E", "#CCBA7C",
           "#E2E2C1", "#DBD83D", "#AA7028", "#BAD8EA", "#70A3BA"]

plot(Raster("nlcd_2016_frederick_md.tif"),
     title = "Land Cover Type", xlabel = "Easting", ylabel = "Northing",
     seriescolor = cgrad(palette, (values .- 12) ./ 84, categorical = true),
     size = (700, 640))

Now, load the array using Omniscape's internal read_raster() function or a function from a GIS Julia package of your choice. read_raster() returns a tuple with the data array, a wkt string containing geographic projection info, and an array containing geotransform values. We'll use the wkt and geotransform later.

land_cover, wkt, transform = Omniscape.read_raster("nlcd_2016_frederick_md.tif", Float64)
(Union{Missing, Float64}[82.0 82.0 … 43.0 43.0; 82.0 82.0 … 43.0 43.0; … ; 82.0 81.0 … 82.0 82.0; 81.0 81.0 … 22.0 82.0], "PROJCS[\"Albers_Conical_Equal_Area\",GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4326\"]],PROJECTION[\"Albers_Conic_Equal_Area\"],PARAMETER[\"latitude_of_center\",0],PARAMETER[\"longitude_of_center\",0],PARAMETER[\"standard_parallel_1\",29.5],PARAMETER[\"standard_parallel_2\",45.5],PARAMETER[\"false_easting\",0],PARAMETER[\"false_northing\",0],UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],AXIS[\"Easting\",EAST],AXIS[\"Northing\",NORTH]]", [1.56369e6, 30.0, 0.0, 1.98666e6, 0.0, -30.0])

The next step is to create a resistance reclassification table that defines a resistance value for each land cover value. Land cover values go in the left column, and resistance values go in the right column. In this case, we are modeling forest connectivity, so forest classes receive the lowest resistance score of one. Other "natural" land cover types are assigned moderate values, and human-developed land cover types are assigned higher values. Medium- to high-intensity development are given a value of missing, which denotes infinite resistance (absolute barriers to movement).

# Create the reclassification table used to translate land cover into resistance
reclass_table = [
    11.	100; # Water
    21	500; # Developed, open space
    22	1000; # Developed, low intensity
    23	missing; # Developed, medium intensity
    24	missing; # Developed, high intensity
    31	100; # Barren land
    41	1; # Deciduous forest
    42	1; # Evergreen forest
    43	1; # Mixed forest
    52	20; # Shrub/scrub
    71	30; # Grassland/herbaceous
    81	200; # Pasture/hay
    82	300; # Cultivated crops
    90	20; # Woody wetlands
    95	30; # Emergent herbaceous wetlands
]
15×2 Matrix{Union{Missing, Float64}}:
 11.0   100.0
 21.0   500.0
 22.0  1000.0
 23.0      missing
 24.0      missing
 31.0   100.0
 41.0     1.0
 42.0     1.0
 43.0     1.0
 52.0    20.0
 71.0    30.0
 81.0   200.0
 82.0   300.0
 90.0    20.0
 95.0    30.0

Next, we define the configuration options for this model run. See the Settings and Options section in the User Guide for more information about each of the configuration options.

# Specify the configuration options
config = Dict{String, String}(
    "radius" => "100",
    "block_size" => "21",
    "project_name" => "md_nlcd_omniscape_output",
    "source_from_resistance" => "true",
    "r_cutoff" => "1", # Only forest pixels should be sources
    "reclassify_resistance" => "true",
    "calc_normalized_current" => "true",
    "calc_flow_potential" => "true"
)
Dict{String, String} with 8 entries:
  "calc_normalized_current" => "true"
  "calc_flow_potential"     => "true"
  "radius"                  => "100"
  "source_from_resistance"  => "true"
  "block_size"              => "21"
  "r_cutoff"                => "1"
  "project_name"            => "md_nlcd_omniscape_output"
  "reclassify_resistance"   => "true"

Finally, compute connectivity using run_omniscape(), feeding in the configuration dictionary, the resistance array, the reclass table, as well as the wkt and geotransform information loaded earlier. Passing in the wkt and geotransform, along with true for the write_outputs argument, will allow Omniscape to write the outputs as properly projected rasters. run_omniscape will print some information to the console and show progress, along with an ETA, in the form of a progress bar.

currmap, flow_pot, norm_current = run_omniscape(config,
                                                land_cover,
                                                reclass_table = reclass_table,
                                                wkt = wkt,
                                                geotransform = transform,
                                                write_outputs = true)
(Union{Missing, Float64}[0.002725895636045624 0.00949946238540265 … 0.7701753044818226 0.5588858207982138; 0.008791599816740273 0.01685623544448697 … 1.1963152183457202 0.8100820554386359; … ; 0.22307355874067347 0.6216620910614019 … 0.9836258338404574 0.44301293554578414; 0.06824326358051583 0.12451438738405202 … 0.3755969475119765 0.13747736528383858], Union{Missing, Float64}[0.22851654463598325 0.7582581265070771 … 0.8429397856671239 0.5590075422826745; 0.7733371879936655 1.3188749872253172 … 1.2799958538141314 0.7890368128348983; … ; 0.13214272974421676 0.2369979661809652 … 0.4059086986242947 0.2336255195256739; 0.04552497135409577 0.1525431391804039 … 0.23018154785287884 0.06914308189257572], Union{Missing, Float64}[0.011928657683792022 0.012528006035572619 … 0.913677723578187 0.9997822543074041; 0.011368391373430611 0.012780768160558982 … 0.9346242917748057 1.026672066830602; … ; 1.6881258558262553 2.6230693076358205 … 2.423268673901695 1.8962523291343605; 1.4990292481397938 0.8162568834826198 … 1.631742209640715 1.988302539036813])

You'll see that outputs are written to a new folder called "md_nlcd_omniscape_output". This is specified by the "project_name" value in config above. The cumulative current map will always be called "cum_currmap.tif", and it will be located in the output folder. We also specified in the run configuration that flow potential and normalized current should be computed as well. These are called "flow_potential.tif" and "normalized_cum_currmap.tif", respectively. See Outputs for a description of each of these outputs.

Now, plot the outputs. Load the outputs into Julia as spatial data and plot them.

First, the cumulative current map:

current = Raster("md_nlcd_omniscape_output/cum_currmap.tif")
plot(current,
     title = "Cumulative Current Flow", xlabel = "Easting", ylabel = "Northing",
     seriescolor = cgrad(:inferno, [0, 0.005, 0.03, 0.06, 0.09, 0.14]),
     size = (600, 550))

Cumulative current flow representing forest connectivity. Note that areas in white correspond to built up areas (NLCD values of 23 and 24) that act as absolute barriers to movement.

Next, flow potential. This map shows what connectivity looks like under "null" conditions (resistance equals 1 for the whole landscape).

fp = Raster("md_nlcd_omniscape_output/flow_potential.tif")
plot(fp,
     title = "Flow Potential", xlabel = "Easting", ylabel = "Northing",
     seriescolor = cgrad(:inferno, [0, 0.005, 0.03, 0.06, 0.09, 0.14]),
     size = (700, 640))

Flow potential, which shows what connectivity would look like in the absence of barriers to movement. The blocking that you can see is an artifact of setting a large block_size to make the example run faster. Set a smaller block_size to reduce/remove this issue.

Finally, map normalized current flow, which is calculated as cumulative current divided by flow potential.

normalized_current = Raster("md_nlcd_omniscape_output/normalized_cum_currmap.tif")
plot(normalized_current,
     title = "Normalized Current Flow", xlabel = "Easting", ylabel = "Northing",
     seriescolor = cgrad(:inferno, [0, 0.005, 0.03, 0.06, 0.09, 0.14]),
     size = (700, 640))

Normalized cumulative current. Values greater than one indicate areas with channelized/bottlenecked flow. Values around 1 (cumulative current ≈ flow potential) indicate diffuse current. Values less than 1 indicate impeded flow.