knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) required <- c("viridisLite") if (!all(sapply(required, requireNamespace, quietly = TRUE))) { knitr::opts_chunk$set(eval = FALSE) } do.call(knitr::read_chunk, list(path = "scripts/example-maze.R"))
This example shows how to use and interpret different absorbing Markov chain metrics with a perfect maze. A perfect maze is a maze that has no loops, every point is reachable, and there is only a single path between any two points. The motivation for this type of example is that is fun and interesting while having very simple properties that make it easy to visually and numerically interpret the results of the different metrics available in the package.
The complete code for this series is available on GitHub.
<<1_library_1>>
Create a function to reduce redundant plotting code and make the examples easier to read:
<<1_setup_1>>
Create a simple color palette for when only one or two colors are needed in a figure:
<<1_setup_2>>
Setup up a resistance map representing the maze and use the gdistance
package to get information about the solution (the shortest path). Assume the start is the top left and the finish is the bottom right:
<<1_setup_3>>
Create an absorption map where the finish point is the only source of absorption. It will have an absorption value of 1.0
, which means that once this point is entered, it cannot be left for another cell in the maze:
<<1_setup_4>>
Given the nature of some of the calculations performed by the package, it's sometimes possible for floating point precision issues to arise where equality comparisons between two decimal values don't yield the expected results. It's an unfortunate limitation of all computer hardware and is why functions like all.equal()
exist in base R. Unfortunately, there isn't a base function available for the specific comparisons we want to perform, so we will manually have to do it. To do so, we will need a tolerance value and will use the default tolerance used by all.equal()
:
<<1_setup_5>>
samc
objectWith the resistance and absorption maps prepared, a samc object can be created:
<<1_setup_6>>
The model represented by this samc object has been set up to assume a simple random walk. There is no "memory" of the past to stop the individual from going back to dead ends, there is no ability to "look ahead" and see dead ends, and the individual will always move to a different cell every time step.
Note the use of only four directions. This prevents diagonal movements in the maze, which in turn will have important consequences for short-term metrics (discussed later).
The start and finish locations are obtained from the samc object using the locate()
function. It's important to remember that the results from xyFromCell()
(used above for the RasterLayer object) do NOT work for samc objects. Although the results from both functions may be equivalent in certain cases, they generally will not be equivalent and cannot be used interchangeably.
We will start with determining how long, on average, it would be expected for a single individual to finish the maze. Formally, the survival()
function calculates the expected time to absorption, but since the maze has only one absorption point representing the finish point, this means that, in this example, survival()
can be interpreted as the expected time to finish the maze:
<<1_ttf_1>>
The result is a vector with the expected time to absorption, or finish, for every point in the maze, not just the top left start point. The start location can be used to extract that information from the vector:
<<1_ttf_2>>
When there is only a single point of total absorption, like in this example, survival()
and cond_passage()
are nearly identical when the destination for cond_passage()
is set to the point of absorption (the finish point):
<<1_ttf_3>>
It turns out that the cell in the map corresponding to our finish point is not an absorbing state; it's still a transient state. Absorption occurs one time step later when the individual is removed from the maze since it can't go anywhere else in our example. So cond_passage()
reports how long it takes to reach the final cell, and survival()
tells us how long it takes us to reach the final cell and then be removed from it.
A couple of things to keep in mind:
- survival()
will have different results and a different interpretation for the maze in more complicated scenarios with multiple points of absorption.
- cond_passage()
normally does not work when there are states that lead to total absorption. The only exception is when there is a single instance of a state leading to total absorption present and the dest
parameter is set to it. In that case, the absorption state is effectively ignored. This is why cond_passage()
works as shown in this example, but attempting to change the dest
parameter to a different location will cause it to fail. cond_passage()
can still be used for other dest
values, but first, the absorption value would have to be changed to a value less than 1
.
If we're interested in knowing the probability that a particular cell (or cells) is visited at least once, then we can use the dispersal()
function:
<<1_pov_1>>
To complete the maze, the individual has to visit every cell along the path to the exit, so all of those cells will have a probability of 1.0
. The farther away from this path a cell is located, the lower the probability it will be visited. Additionally, the closer the individual gets to the finish, the less likely they will spend time taking incorrect routes before stumbling upon the finish. There's also the possibility of scenarios like the one where the individual is near the finish and then manages to stumble back to the start. These different aspects contribute to more time spent near the start point, on average, which in turn means that cells near the start have a higher probability of being visited.
The previous paragraph hinted that the results from the dispersal()
function can be used to identify the route through the maze:
<<1_pov_2>>
Like the survival()
function, there is an important caveat: The visitation probabilities don't include time 0
. So while it looks like the starting cell might have a probability of 1
in the first figure, it's slightly less and it just wasn't apparent with the color scale:
<<1_pov_3>>
The package can be used to see how many times each cell in the maze is expected to be visited on average. This is done using the visitation()
metric:
<<1_visit_1>>
Since the finish point leads to total absorption, it will only be visited once:
<<1_visit_2>>
Using the distribution()
function, the location of an individual in the maze can be predicted for a given time:
<<1_loc_1>>
There's an odd pattern here that can be emphasized by incrementing the time from 20 to 21:
<<1_loc_2>>
Earlier it was mentioned that using only 4 directions for the transition function would have consequences. Without either diagonal transitions or some form of fidelity (the possibility of an individual not moving), there will be an alternating pattern where a cell can and then cannot be occupied. It's easiest to reason about the effect by running the same function for time steps 1-5 and visualizing it, an exercise that will be left to those interested.
There are a couple of metrics that have not been covered yet: absorption()
and mortality()
. For the example provide thus far, they don't do anything useful. mortality()
would only report a 100% probability of absorption at the finish point; it will be more useful when there are multiple locations with absorption probabilities. absorption()
is only useful when there are different types of absorption.
Many of the metrics offer a more advanced and flexible option for setting an initial state in the absorbing Markov chain. Inputs to the init
parameter (short for initial state) can essentially be used as an alternative to specifying a singular start point. Here is an example init
map that represents the scenario that has been presented so far:
<<1_init_1>>
Using the survival()
metric, it produces the same result as before:
<<1_init_2>>
But now, more interesting scenarios can be tested. For example, let's start the maze with 3 individuals:
<<1_init_3>>
The interpretation becomes a little trickier here. The survival()
metric is returning the cumulative time spent in the maze for all the individuals. This means that with 3 individuals starting at the beginning, 3 times as much time is going to be spent in the maze, but per individual, it will still take the same number of time steps to finish as before:
<<1_init_4>>
Here's a different scenario with 3 individuals again, but now they start in different corners of the maze:
<<1_init_5>>
Again, the cumulative time spent in the maze is larger than reported in the original scenario with one individual. However, this time the average time per individual spent in the maze decreases:
<<1_init_6>>
This is because the additional individuals are now located in corners that are substantially closer to the finish point, so they would be expected to find it faster on average. Based on where each individual starts, it would be reasonable to expect that the individual starting in the bottom left has the biggest advantage. One way to try and visualize this is with the distrbution()
metric:
<<1_init_7>>
A good exercise for the reader is to apply the concepts of the animations vignette to this figure.
In future parts, many different modifications will be made to the maze to explore how they change the results of different metrics and how the different metrics are related. These modifications include the use of fidelity to simulate scenarios where the individual pauses to think about their next decision, modifying the resistance input to simulate "looking ahead" to perform dead-end avoidance, and modifying the absorption input to simulate different sources of absorption such as lethal traps.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.