Skip to content

An introduction to BiodiversityObservationNetworks

In this vignette, we will walk through the basic functionalities of the package, by generating a random uncertainty matrix, and then using a seeder and a refiner to decide which locations should be sampled in order to gain more insights about the process generating this entropy.

using BiodiversityObservationNetworks
using NeutralLandscapes
using CairoMakie

In order to simplify the process, we will use the NeutralLandscapes package to generate a 100×100 pixels landscape, where each cell represents the entropy (or information content) in a unit we can sample:

U = rand(MidpointDisplacement(0.5), (100, 100))
heatmap(U)

In practice, this uncertainty matrix is likely to be derived from an application of the hyper-parameters optimization step, which is detailed in other vignettes.

The first step of defining a series of locations to sample is to use a BONSeeder, which will generate a number of relatively coarse proposals that cover the entire landscape, and have a balanced distribution in space. We do so using the BalancedAcceptance sampler, which can be tweaked to capture more (or less) uncertainty. To start with, we will extract 200 candidate points, i.e. 200 possible locations which will then be refined.

pack = seed(BalancedAcceptance(; numpoints = 200), U);
(CartesianIndex[CartesianIndex(52, 56), CartesianIndex(77, 5), CartesianIndex(14, 38), CartesianIndex(64, 71), CartesianIndex(89, 49), CartesianIndex(33, 60), CartesianIndex(21, 8), CartesianIndex(71, 42), CartesianIndex(46, 75), CartesianIndex(55, 86)  …  CartesianIndex(54, 68), CartesianIndex(29, 12), CartesianIndex(79, 46), CartesianIndex(42, 57), CartesianIndex(11, 5), CartesianIndex(61, 38), CartesianIndex(36, 72), CartesianIndex(23, 50), CartesianIndex(73, 83), CartesianIndex(98, 61)], [0.3802615508260841 0.4092031652768475 … 0.4298395561838507 0.46646427592566564; 0.42121022208112524 0.3885120204257992 … 0.3708214609433243 0.4135299014176066; … ; 0.36597488743604767 0.33663283808829875 … 0.7494154403796882 0.7850192951525803; 0.3728421703537687 0.3945924810553696 … 0.7551024240571974 0.7322412724520643])

The output of a BONSampler (whether at the seeding or refinement step) is always a tuple, storing in the first position a vector of CartesianIndex elements, and in the second position the matrix given as input. We can have a look at the first five points:

first(pack)[1:5]
5-element Vector{CartesianIndex}:
 CartesianIndex(52, 56)
 CartesianIndex(77, 5)
 CartesianIndex(14, 38)
 CartesianIndex(64, 71)
 CartesianIndex(89, 49)

Although returning the input matrix may seem redundant, it actually allows to chain samplers together to build pipelines that take a matrix as input, and return a set of places to sample as outputs; an example is given below.

The positions of locations to sample are given as a vector of CartesianIndex, which are coordinates in the uncertainty matrix. Once we have generated a candidate proposal, we can further refine it using a BONRefiner – in this case, AdaptiveSpatial, which performs adaptive spatial sampling (maximizing the distribution of entropy while minimizing spatial auto-correlation).

candidates, uncertainty = pack
locations, _ = refine(candidates, AdaptiveSpatial(; numpoints = 50), uncertainty)
locations[1:5]
5-element Vector{CartesianIndex}:
 CartesianIndex(99, 65)
 CartesianIndex(49, 68)
 CartesianIndex(47, 65)
 CartesianIndex(98, 61)
 CartesianIndex(94, 59)

The reason we start from a candidate set of points is that some algorithms struggle with full landscapes, and work much better with a sub-sample of them. There is no hard rule (or no heuristic) to get a sense for how many points should be generated at the seeding step, and so experimentation is a must!

The previous code examples used a version of the seed and refine functions that is very useful if you want to change arguments between steps, or examine the content of the candidate pool of points. In addition to this syntax, both functions have a curried version that allows chaining them together using pipes (|>):

locations =
    U |>
    seed(BalancedAcceptance(; numpoints = 200)) |>
    refine(AdaptiveSpatial(; numpoints = 50)) |>
    first
50-element Vector{CartesianIndex}:
 CartesianIndex(92, 73)
 CartesianIndex(93, 77)
 CartesianIndex(95, 76)
 CartesianIndex(89, 70)
 CartesianIndex(86, 69)
 CartesianIndex(84, 71)
 CartesianIndex(81, 68)
 CartesianIndex(85, 64)
 CartesianIndex(82, 62)
 CartesianIndex(81, 63)
 ⋮
 CartesianIndex(84, 92)
 CartesianIndex(47, 74)
 CartesianIndex(31, 29)
 CartesianIndex(18, 96)
 CartesianIndex(25, 66)
 CartesianIndex(75, 100)
 CartesianIndex(50, 1)
 CartesianIndex(100, 35)
 CartesianIndex(52, 12)

This works because seed and refine have curried versions that can be used directly in a pipeline. Proposed sampling locations can then be overlayed onto the original uncertainty matrix:

plt = heatmap(U)
#scatter!(plt, [x[1] for x in locations], [x[2] for x in locations], ms=2.5, mc=:white, label="")