@machine> julia
user
__(_)_ | Documentation: https://docs.julialang.org
_ _ | (_) (_) |
(_) | |_ __ _ | Type "?" for help, "]?" for Pkg help.
_ _ _| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.7.0 (2021-11-30)
/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
_|__/ |
> julia
NeutralLandscapes.jl Walkthrough
This document provides a tutorial in using NeutralLandscapes.jl
, the julia
library for simulating neutral landscapes. Neutral landscapes have become a common tool in landscape ecology, primarily as a null model of spatial data (Gardner et al. 1987). Here we present a package for neutral-landscape generation in julia
, which both is considerably faster than both existing R
and python
packages, but also implements novel methods of simulating temporal change in landscapes with fixed statistical properties.
In this walkthrough we’ll cover (1) installing julia
and NeutralLandscapes.jl, (2) using different neutral landscape generators using the rand
function, (3) simulating temporal change in neutral landscapes (a feature unique to NL.jl
), and (4) simulating the occurrence of a species tracking its niche over time, producing the following output.
1. Getting Started
In this section we’ll install julia
(if you don’t already have it) and the NeutralLandscapes.jl
package.
Installing julia
First, if you don’t have julia
installed on your machine, install it from https://julialang.org/downloads/. (Note if you are using an M1 Mac, v1.8 is recommended. Otherwise either v1.7 or v1.8 are both fine).
There are two ways to interact with julia
. You can either open the Read-Evaluate-Print-Loop (REPL) by trying julia
in a terminal, e.g.
The REPL acts just like a console in R. The second method is to use julia to run a .jl
script, e.g.
@machine> julia script.jl user
Installing NeutralLandscapes.jl
The easiest way to install NeutralLandscapes.jl
, or any julia
package is through the REPL. Open the REPL by typing julia
in a terminal . The julia
shell can operate in a few different modes. By default, the shell runs in julia
mode, where the prompt looks like
> julia
You can enter the package-management mode by press the square-bracket key (]
), which changes to prompt to the package mode:
.7) pkg> (v1
To install NeutralLandscapes, we run
.7) pkg> add NeutralLandscapes (v1
and then once done installing, hit backspace to exit Pkg
mode and to return to the julia
console. Read more about the Julia REPL modes here and Pkg
, the package-management system for julia, here.
2. Making Neutral Landscapes
In this section we’ll cover the syntax for generating neutral landscapes, starting with a simple gradient and then showing the various other types of landscape types we can generate.
Simple example
We start by loading the package using
using NeutralLandscapes
Now let’s create our first neutral landscape. We’ll create a linear gradient using the PlanarGradient
generator. We can search for the documentation by entering the help mode of the REPL by pressing ?
, or by using the ?
function.
?(PlanarGradient)
search: PlanarGradient
PlanarGradient <: NeutralLandscapeMaker
PlanarGradient(; direction=360rand())
PlanarGradient(direction)
This type is used to generate a planar gradient landscape, where values change as a bilinear function of the x and y coordinates. The direction is expressed as a floating point value, which will be in [0,360]. The inner constructor takes the mod of the value passed and 360, so that a value that is out of the correct interval will be corrected.
Using the above process we can understand what arguments to each neutral landscape generator are, and what they do.
Using rand
to generate landscapes
To use PlanarGradient
to generate a landscape, we use the rand
function. The rand
function is used for all sorts of random generation in julia
. For example, just rand()
generates a random number between 0 and 1, e.g.
rand()
0.46110243791553074
Although rand
can be called with different arguments to run a different version of the function that corresponds to the type of the input arguments. For example, using the Distributions
package allows us to draw random samples from various distributions, e.g. a Poisson
distribution with mean 7
.
using Distributions
rand(Poisson(7))
8
or a set of 10 independent draws from a standard Normal
distribution.
rand(Normal(0,1), 10)
10-element Vector{Float64}:
-1.0750868899665584
1.4726498920951105
0.08334169367764765
1.001596634832882
0.41825145919788725
0.3409662777991979
0.968971026996396
-0.7741079564132628
1.9897432432718793
-0.2856305633137191
This is possible through a feature called multiple dispatch, where a function with the same name (rand
, in this case) will behave differently depending on the arguments passed to it (similar to overloading in C/C++).
The syntax for generating random neutral landscapes is rand(NEUTRAL_LANDSCAPE_GENERATOR, X_SIZE, Y_SIZE)
. For example, to generate a PlanarGradient
landscape with dimensions 200 pixels x 100 pixels, we run
= rand(PlanarGradient(), (200,100)); neutrallandscape
which we plot with
using Plots
heatmap(neutrallandscape)
More complicated NeutralLandscapeMakers
NeutralLandscapes.jl
supports a large number of methods for generating neutral landscapes. Feature parity is with the Python package for neutral landscapes (Etherington, Holland, and O’Sullivan 2015).
Gradients
The simplest models are gradients. We implement NoGradient
, which is just random, as well as PlanarGradient
(a linear gradient), EdgeGradient
(a symetric gradient around a line), and DistanceGradient
(gradient as a function of distance from a given point).
= (100,100)
siz = rand(NoGradient(), siz)
random = rand(PlanarGradient(), siz)
planar = rand(EdgeGradient(), siz)
edge
= falses(siz)
obj 10:25, 10:25] .= true
obj[= rand(DistanceGradient(findall(vec(obj))), siz)
dist
plot(heatmap(random),
heatmap(edge),
heatmap(dist),
heatmap(planar))
Clustering
Clustering methods (RectangularCluster
, NearestNeighborElement
, and NearestNeighborCluster
) produce discrete boundaries. These are useful for simulating landcover data, or other data with hard boundaries.
= rand(RectangularCluster(4, 8), siz)
rect = rand(NearestNeighborElement(200), siz)
nne = rand(NearestNeighborCluster(0.4), siz)
nnc plot(heatmap(rect), heatmap(nne), heatmap(nnc))
Spatially Autocorrelation
Fractal Brownian motion has a long history in ecology (Milne 1992). NL.jl implements two similar but slightly different method for FBM generation, DiamondSquare
and MidpointDisplacement
. Each take a parameter H
, between 0 and 1, which describes the level of autocorrelation.
= rand(MidpointDisplacement(0.75), siz)
mpd = rand(DiamondSquare(0.25), siz)
ds plot(heatmap(mpd), heatmap(ds), size=(700,300))
Classification
Using the classify
function, we can bin values into a set of discrete categories. This can be useful for simulating discrete patches in otherwise uninhabitable landscape.
= classify(ds, [1-0.5, 0.5])
clas1 = classify(ds, [1-0.75, 0.75])
clas2 = classify(mpd, ones(3))
clas3 = classify(mpd, [0.2i for i in 0:5])
clas4 plot(heatmap(clas1), heatmap(clas2), heatmap(clas3), heatmap(clas4))
3. Landscapes that change over time
Now we get to the novel part in NeutralLandscapes.jl, which is the ability to simulate change using 3 different methods.
function makeplot(updater)
= rand(PlanarGradient(π/2), 100, 100)
init = update(updater, init, 30)
seq print(length(seq))
plot(heatmap(init, cbar=:none), heatmap(seq[10], cbar=:none), heatmap(seq[20], cbar=:none), heatmap(seq[30], cbar=:none), size=(700,200), layout=(1,4))
end
makeplot (generic function with 1 method)
Temporally variable change
?(TemporallyVariableUpdater)
search: TemporallyVariableUpdater
TemporallyVariableUpdater{D,S} <: NeutralLandscapeUpdater
A NeutralLandscapeUpdater
that has a prescribed level of temporal variation (variability
) and rate of change (rate
), but no spatial correlation in where change is distributed.
= TemporallyVariableUpdater()
tvu makeplot(tvu)
30
4. Simulating occurrence data across Quebec
Now we’ll use NL.jl to simulate the occurrence of a species tracking its niche over time across Quebec.
Masking a NeutralLandscape across Quebec
First we’ll show how NeutralLandscapes.jl
can integrate with SimpleSDMLayers.jl
, the julia
package for species distribution modeling.
using SimpleSDMLayers
using Plots
ENV["SDMLAYERS_PATH"] = "/home/michael/data/";
First we use SimpleSDMLayers.jl to download BioClim data for Quebec
= SimpleSDMPredictor(WorldClim, BioClim; left=-90., right=-55., top=63., bottom=45.) quebec
SDM predictor → 108×210 grid with 15058 Float32-valued cells
Latitudes 45.0 ⇢ 63.0
Longitudes -90.0 ⇢ -55.0
Now we’ll construst a mask to use with NL.jl
= fill(true, size(quebec))
qcmask findall(isnothing, quebec.grid)] .= false; qcmask[
And now we’ll plot several sample NeutralLandscapes masked over Quebec.
= (cbar=:none, frame=:box)
pltsettings plot(
heatmap(rand(MidpointDisplacement(0.8), size(quebec), mask=qcmask); pltsettings...),
heatmap(rand(PlanarGradient(), size(quebec), mask=qcmask); pltsettings...),
heatmap(rand(PerlinNoise((4,4)), size(quebec), mask=qcmask); pltsettings...),
heatmap(rand(NearestNeighborElement(30), size(quebec), mask=qcmask); pltsettings...),
=400
dpi )
Defining a niche
Now we’re going to define a niche
function, which returns the occurrence probability of a species with a niche that is a Gaussian with mean nichemean
and standard deviation nichewidth
.
niche(env; nichemean = 0.7, nichewidth = 0.3) = exp(-1*((env-nichemean)/nichewidth)^2)
niche (generic function with 1 method)
And now we’ll write a function to visualize these niches
function nicheplot(; nichemean = 0.7, nichewidth = 0.3)
= 0:10^-3:1
envrange plot(
envrange,=:none,
label=3,
lw="Niche center: $nichemean, Niche width: $nichewidth",
titleniche(x; nichemean=nichemean, nichewidth=nichewidth) for x in envrange],
[="Environmental value",
xlabel="Occurrence Probability",
ylabel=9,
titlefontsize=9,
labelfontsize=:box
frame
)end
nicheplot (generic function with 1 method)
Now let’s plot some sample niches to get a visual sense of how they work.
= 0:10^-3:1
envrange plot(
nicheplot(;nichemean=0.5, nichewidth=0.2),
nicheplot(;nichemean=0.7, nichewidth=0.1),
nicheplot(;nichemean=0.1, nichewidth=0.05),
nicheplot(;nichemean=0.5, nichewidth=0.5),
=(600,600)
size )
Simulating occurrence
Finally, we’ll write the occurrence
function which returns a set of numpoints
coordinates drawn with probabilities proportional to the occurrence probability provided by niche
at each cell in the landscape.
function occurrence(landscape, qcmask, numpoints = 50; kwargs...)
= map(e-> isnan(e) ? 0 : niche(e; kwargs...), landscape)
probabilitymap = probabilitymap ./ sum(probabilitymap)
normalizedprob
= zeros(size(landscape))
occmat = rand(Categorical(vec(normalizedprob)), numpoints)
occind .= 1
occmat[occind] = findall(x->x==1, occmat)
occpoints = [x[2] for x in occpoints], [x[1] for x in occpoints]
longs, lats end
occurrence (generic function with 2 methods)
Now we set an initial condition that is moderately spatially autocorrelated and that is generally higher in the south than the north (think temperature).
= rand(PlanarGradient(0), size(quebec), mask=qcmask)
grad = rand(MidpointDisplacement(0.3), size(quebec), mask=qcmask)
mpd
= blend(grad,mpd)
init heatmap(init)
Now we simulate the temporally changing landscape
= SpatiotemporallyAutocorrelatedUpdater(
svu = 0.03,
rate = 0.03,
variability = MidpointDisplacement(0.5)
spatialupdater
)= normalize(update(svu, init, 30)); seq
and finally simulate occurrence and plot, to produce the final output.
= @animate for (t,s) in enumerate(seq)
anim = occurrence(s, qcmask, 30; nichemean=0.5, nichewidth=0.05)
longs, lats heatmap(s, dpi=150, title = "Year $t", frame=:box, ticks=:none, aspectratio=1, size=(400,300), xlim=(0,220), ylim=(0,110), clim=(0,1))
scatter!(longs, lats, c=:dodgerblue, msw=0, label="")
end
gif(anim, "result.gif", fps=5)
┌ Info: Saved animation to
│ fn = /home/michael/code/NLIntroductions/result.gif
└ @ Plots /home/michael/.julia/packages/Plots/1KWPG/src/animation.jl:114