Quetzal-CoaTL
The Coalescence Template Library
Loading...
Searching...
No Matches
Local Parameters

Environmental Niche Models

Niche models as mathematical expressions

In the field of ecology, the niche refers to the compatibility between a species and a specific environmental condition. It explains how an organism or a population adapts to the availability of resources and competition.

For instance, when resources are plentiful and the presence of predators, parasites, and pathogens is low, the organism or population tends to grow. In locations and times of resources scarcity and high predation, the populations tend to shrink.

The composition and significance of environmental variables that constitute the dimensions of a niche can vary among species, and the importance of specific environmental factors for a species may change depending on geographical and ecological contexts.

Note
In the broader context of this library, the niche model lacks a specific definition as it relies on the preferences and requirements of individual users. What we can understand is that it involves some combination of spatial and temporal factors that are closely intertwined with the demographic process. This justifies the need for a highly adaptable approach for representing the concept of niche, as it is bound to vary depending on the specific species, population, or geographic area under study.

Given this understanding, the primary goal of Quetzal is to provide users with quetzal::expressive, a user-friendly means to effectively combine geospatial and temporal elements. This enables users to establish meaningful mathematical connections between a diverse and heterogeneous landscape and the underlying demographic processes, based on their own perspectives and preferences. Generally speaking, users will want to use quetzal::expressive to mathematically compose spatio-temporal expressions with a signature double (location_type, time_type) until they reach a satisfying definition of the local demographic parameter of interest.

Mathematical composition in C++

Mathematical composition in C++ is not trivial as unlike languages, C++ does not understand mathematical expressions and their composition. In this strongly-typed language, you can not simply multiply the integer 42 and the function \( f(x,y) = x^y \) and expect it to work. Indeed, if you try to rescale a lambda function, e.g.:

int a = 42;
auto f = [](auto x, auto y){return std::pow(x,y); };
my_expression = a * f; // error

it will not compile. As the compiler does not know natively what the rules are to multiply a number with whatever type f is, it will (verbosely) insult you saying the operator*() is not defined for the type int and an anonymous type (the lambda function).

Mathematical composition with quetzal::expressive

You need a library to tell the compiler what to do. This is where quetzal::expressive comes handy: it allows you to add, substract, multiply, or compose mathematical expressions by giving them a uniform interface.

In the context of demographic models, my_expression in the example below could represent for example r(x,t) the growth rate at location x at time t. Being able to compose several smaller expressions into bigger expressions gives much freedom to users who can for example define r in one part on the program and compose it at a later time with a different expression e.g. k(x,t) the carrying capacity at location x at time t. By treating these local demographic parameters as independent expressions that can be mixed and matched, quetzal::expressive allows great freedom in the implementation details of a spatio-temporal process.

Input

1#include "quetzal/quetzal.hpp"
2#include <cassert>
3#include <cmath> // std::pow
4#include <iostream>
5
6int main()
7{
8 // This is used to transform literals into callables
10 // This is used to make lambdas usable by expressive
12
13 // This object will create little functions that can be called with (double, double) parameters
14 literal_factory<double, double> lit;
15
16 // a is no longer a literal: it's a callable always returning 42
17 auto a = lit(42.);
18 // check a is indeeed callable with (double, double) parameters
19 assert(a(1.0, 3.14) == 42.0);
20
21 // This enriches the lambda interface by giving it access to mathematical operators
22 auto f = use([](double x, double y) { return std::pow(x, y); });
23 // check a is indeeed callable with (double, double) parameters
24 assert(f(1.0, 3.14) == 1.0);
25
26 // Both a and f can now be invoked with the same interface (double, double) -> double
27
28 // We can now compose them easily ...
29 auto my_expression = a * f;
30
31 // ... and call them possibly (much) later eg in the library's internals
32 std::cout << my_expression(1.0, 3.14) << std::endl;
33}
constexpr auto use(F f)
Transforms functions into operator friendly classes.
Definition expressive.hpp:205
Transforms literals into operator friendly classes.
Definition expressive.hpp:263

Output

142

Preparing a suitability landscape for the simulation

Note
The objective of this section is to load a suitability map with quetzal::geography::raster and prepare its integration into the simulation framework with quetzal::expressive.

In the context of ecological niche modeling (ENM), suitability refers to the degree of appropriateness or favorability of a particular environmental condition for a species to thrive and persist. ENM is a technique used in ecology to predict the distribution of species across landscapes based on their known occurrences and environmental variables.

Suitability can be thought of as a spectrum, ranging from conditions that are highly suitable for a species' survival and reproduction to conditions that are unsuitable or even detrimental. ENM attempts to map this spectrum across geographical space by analyzing the relationships between species occurrences and various environmental factors, such as temperature, precipitation, elevation, soil type, and vegetation cover.

It's important to note that suitability does not solely depend on a single environmental variable. Instead, it's a complex interplay of multiple factors that determine whether a species can survive, reproduce, and establish a viable population in a given area. Suitability GIS maps derived from previous analysis steps (see e.g. the niche models in Quetzal-crumbs python package) can then be integrated into the coalescence simulation framework.

The following example code

  • loads a suitability map using quetzal::geography::raster
  • prepares its integration into the simulation framework using quetzal::expressive
  • it gives to the suitability the semantic of a function of space and time f(x,t)
  • it gives it access to mathematical operators with quetzal::expressive::use
  • then it builds upon it to define an (arbitrary) expression of e.g. the growth rate (but it could be friction, or carrying capacity...)

Input

1#include "quetzal/quetzal.hpp"
2#include <iostream>
3
4using namespace quetzal;
5
6int main()
7{
8
9 // defining some useful type aliases
10 using time_type = int;
11 using raster_type = geography::raster<time_type>;
12 using location_type = raster_type::location_descriptor;
13
14 // Transforms simple numbers into functions of space and time
16
17 // The raster has 10 bands that we will assign to 2001 ... 2011.
18 std::vector<time_type> times(10);
19 std::iota(times.begin(), times.end(), 2001);
20
21 // Load the suitability map
22 auto file = std::filesystem::current_path() / "data/suitability.tif";
23 auto raster = raster_type::from_file(file, times);
24
25 // We need to make choices here concerning how NA are handled
26 auto f1 = raster.to_view(); // lightweight functor returning empty optionals for NA
27 auto f2 = [&](location_type x, time_type t) {
28 return f1(x, t).value_or(0.0);
29 }; // remap empty optionals (NA) to 0.0 suitability value
30 auto f3 = expressive::use(f2); // f3 has now access to math operators
31 auto r = lit(2) * f3 + lit(1); // the growth rate r is a linear function of the suitability
32
33 // r can be later invoked with a location_type and a time_type argument
34 auto x = *raster.locations().begin();
35 auto t = *raster.times().begin();
36 std::cout << "r( x = " << x << ", t = " << t << " ) = " << r(x, t) << std::endl;
37}
Discrete spatio-temporal variations of an environmental variable.
Definition raster.hpp:38
Simulation of coalescence-based models of molecular evolution.
Definition coalescence.hpp:21

Output

1r( x = 0, t = 0 ) = 1.67259

Adjusting a suitability map using a Digital Elevation Model

Note
The objective of this section is to load both a suitability map and a Digital Elevation Model (DEM) with quetzal::geography::landscape and prepare their integration into the simulation framework with quetzal::expressive by dynamically modulating the suitability value as a function of the local elevation.

Why separating Elevation from Suitability ?

It is a common approach to pack a wealth of information into a sole raster file. For instance, this can involve using a single friction file and designating ocean cells with NA values, and obstacles for dispersal with values of 1. Another way is to try to inject elevational data into the suitability map during the Ecological Niche Modeling step.

While these approaches are feasible and occasionally advantageous, they do not consistently represent the optimal or most adaptable solution.

In a broader sense, it's advisable to consider each Geographic Information System (GIS) input file as a means of representing a distinct attribute of the landscape such as suitability, friction, or elevation, utilizing quetzal::geography::landscape to read and align these input files then utilizing quetzal::expressive to adjust or combine these quantities according to the user's modeling intentions.

As an illustration we will show in this example how to adjust the stochasticity of a suitability-driven process as a function of the elevation model. This case and its variations can be applicable in various contexts:

  • Conveniently enforcing a threshold value beyond which the suitability undergoes a significant change (e.g., becoming 0). This approach is valuable for examining sky-island systems or for easily estimating an isoline by dynamically adjusting the cutoff parameter value at runtime, as opposed to altering input files for every simulation run.
  • Depicting the likelihood of a propagule reaching a nunatak, that is a small-scale suitable shelter protruding from the snow sheet (or ice cap). Typically nunataks are the only places where plants can survive in these environments.

Input

1#include "quetzal/quetzal.hpp"
2#include <iostream>
3#include <random>
4
5using namespace quetzal;
6
7int main()
8{
9
10 // defining some useful type aliases
11 using key_type = std::string;
12 using time_type = int;
13 using landscape_type = geography::landscape<key_type, time_type>;
14 using location_type = landscape_type::location_descriptor;
15
16 // Load the suitability map
17 auto file1 = std::filesystem::current_path() / "data/suitability.tif";
18 auto file2 = std::filesystem::current_path() / "data/elevation.tif";
19
20 // The raster has 10 bands that we will assign to 2001 ... 2011.
21 std::vector<time_type> times(10);
22 std::iota(times.begin(), times.end(), 2001);
23
24 // Makes sure the rasters are aligned
25 auto landscape = landscape_type::from_file({{"suit", file1}, {"DEM", file2}}, times);
26
27 // lightweight functor returning empty optionals where NA
28 auto s_view = landscape["suit"].to_view();
29 auto e_view = landscape["DEM"].to_view();
30
31 // We need to make choices here concerning how NA are handled
32 auto suit = expressive::use([&](location_type x, time_type t) { return s_view(x, t).value_or(0.0); });
33 auto elev = expressive::use([&](location_type x, time_type t) { return e_view(x, t).value_or(0.0); });
34
35 std::random_device rd; // a seed source for the random number engine
36 std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd()
37
38 // 1) hostile environment above an isoline: particularly useful if 123.0 is a randomly sampled parameter to be
39 // estimated
40 auto isoline_suitability = [&](location_type x, time_type t) {
41 return (elev(x, t) >= 123.0) ? 0.0 : suit(x, t);
42 };
43
44 // 2) small-scale suitable ice-free shelters randomly popping above the snow cover at high-altitude
45 auto nunatak_suitability = [&](location_type x, time_type t) {
46 std::bernoulli_distribution d(0.1); // give "false" 9/10 of the time
47 bool is_nunatak = d(gen);
48 return (elev(x, t) >= 123.0) ? static_cast<double>(is_nunatak) * suit(x, t) : suit(x, t);
49 };
50
51 // expressions can be passed to a simulator core and later invoked with a location_type and a time_type argument
52 auto x = *landscape.locations().begin();
53 auto t = *landscape.times().begin();
54
55 std::cout << "Suitability w/ isoline ( x = " << x << ", t = " << t << " ) = " << isoline_suitability(x, t)
56 << std::endl;
57 std::cout << "Suitability w/ shelter ( x = " << x << ", t = " << t << " ) = " << nunatak_suitability(x, t)
58 << std::endl;
59}
Discrete spatio-temporal variations of a set of environmental variables.
Definition landscape.hpp:42

Output

1Suitability w/ isoline ( x = 0, t = 0 ) = 0
2Suitability w/ shelter ( x = 0, t = 0 ) = 0

Defining a friction model for trans-oceanic dispersal events

Note
The objective of this section is to load both a suitability map and a Digital Elevation Model (DEM) with quetzal::geography::landscape and prepare their integration into the simulation framework with quetzal::expressive and account for trans-oceanic dispersal events by dynamically defining the friction and the carrying capacity of a cell as functions of the suitability and elevation value.

Drafting events, in the context of biological dispersal, refer to a phenomenon where organisms are carried across large distances by clinging to or being transported by other organisms, objects, or air currents. This can be thought of as a form of passive dispersal where an organism takes advantage of external forces to move beyond its usual range.

The term "drafting" is borrowed from concepts in physics. In biology, this concept is applied to scenarios where organisms, often small and lightweight, catch a ride on other organisms (like birds, insects, or larger animals), objects (like debris), or wind currents. Drafting events can play a significant role in the dispersal of organisms, especially those with limited mobility.

These drafting events provide an opportunity for organisms to reach new areas that they might not be able to access through their own locomotion. It's an interesting ecological phenomenon that highlights the intricate ways in which different species interact and influence each other's distribution and survival.

The general idea is to define lambda expressions that embed stochastic aspects of the model, while preserving the callable interface (space, time) -> double.

Input

1#include "quetzal/quetzal.hpp"
2#include <iostream>
3#include <random>
4
5using namespace quetzal;
6
7int main()
8{
9
10 // defining some useful type aliases
11 using key_type = std::string;
12 using time_type = int;
13 using landscape_type = geography::landscape<key_type, time_type>;
14 using location_type = landscape_type::location_descriptor;
15
16 // Load the suitability map
17 auto file1 = std::filesystem::current_path() / "data/suitability.tif";
18 auto file2 = std::filesystem::current_path() / "data/elevation.tif";
19
20 // The raster has 10 bands that we will assign to 2001 ... 2011.
21 std::vector<time_type> times(10);
22 std::iota(times.begin(), times.end(), 2001);
23
24 // Makes sure the rasters are aligned
25 auto landscape = landscape_type::from_file({{"suit", file1}, {"DEM", file2}}, times);
26
27 // lightweight functor returning empty optionals where NA
28 auto s_view = landscape["suit"].to_view();
29 auto e_view = landscape["DEM"].to_view();
30
31 // We need to make choices here concerning how NA are handled
32 auto suit = expressive::use([&](location_type x, time_type t) { return s_view(x, t).value_or(0.0); });
33 auto elev = expressive::use([&](location_type x, time_type t) { return e_view(x, t).value_or(0.0); });
34
35 std::random_device rd; // a seed source for the random number engine
36 std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd()
37
38 auto rafting_capacity = [&](location_type x, time_type t) {
39 std::bernoulli_distribution d(0.1); // give "false" 9/10 of the time
40 if (suit(x, t) == 0.0 and elev(x, t) == 0.0) // ocean cell case
41 return static_cast<double>(d(gen)) * 2; // will (rarely) allow 2 individuals to survive in the ocean cell
42 else if (suit(x, 0) == 0.0 and elev(x, t) > 0.0) // unhabitable continental cell case
43 return 0.0; // suitability is minimal, so should be the capacity
44 else // habitable continental cells
45 return 100. * suit(x, 0); // simple rescaling by the max number of individuals in a cell
46 };
47
48 auto rafting_friction = [&](location_type x, time_type t) {
49 if (suit(x, t) == 0.0 and elev(x, t) == 0.0) // ocean cell case
50 return 0.0; // the raft should move freely
51 else if (suit(x, 0) == 0.0 and elev(x, t) > 0.0) // hostile continental cell case
52 return 1.00; // max friction as the cell is not attractive
53 else // favorable continental cell case
54 return 1.0 - suit(x, 0); // higher the suitability, easier the travel
55 };
56
57 // expressions can be passed to a simulator core and later invoked with a location_type and a time_type argument
58 auto x = *landscape.locations().begin();
59 auto t = *landscape.times().begin();
60
61 std::cout << "Friction f( x = " << x << ", t = " << t << " ) = " << rafting_friction(x, t) << std::endl;
62 std::cout << "Carrying capacity K( x = " << x << ", t = " << t << " ) = " << rafting_capacity(x, t) << std::endl;
63}

Output

1Friction f( x = 0, t = 0 ) = 0.663704
2Carrying capacity K( x = 0, t = 0 ) = 33.6296