# environment ====
library(shiny)
library(ggplot2)
library(reshape2)
<- 1000 N_grid
forest-fire model
Forest-fire models are not models of forest fires. Instead they’re models through which we can explore dynamic systems. This is a simple R
Shiny app that takes input probabilities for trees and fires; the app is rendered here and was inspired by an artist residency.
First we set the environment, including N_grid
which is the size of our grid - N_grid
= 1000 is equivalent to a 100x100
grid - and make a set of functions.
The 1st function is to create a grid (the land where trees grow)
# Initialize the grid
# 0 = empty, 1 = tree, 2 = fire, 3 = cooling (empty but can't grow tree yet)
<- function(size) {
data_init_forest list(
grid = matrix(0, nrow = size, ncol = size),
stats = list(
total_trees = 0,
fires_started = 0,
trees_burned = 0
)
) }
The 2nd function is for tree growth. We specify that trees can only grow on empty cells.
# Update empty cells to trees based on probability
<- function(forest, p_growth) {
grow_trees <- forest$grid
grid # Only grow trees in cells that are empty (0) but not cooling (3)
<- which(grid == 0 & runif(length(grid)) < (p_growth / N_grid))
new_trees
if (length(new_trees) > 0) {
<- 1
grid[new_trees] $stats$total_trees <- forest$stats$total_trees + length(new_trees)
forest
}
$grid <- grid
forestreturn(forest)
}
The 3rd function is for fires to start. We specify that fires can only start on cells with a tree.
# Start fires in trees based on probability
<- function(forest, p_fire) {
start_fires <- forest$grid
grid <- which(grid == 1 & runif(length(grid)) < (p_fire / N_grid))
new_fires
if (length(new_fires) > 0) {
<- 2
grid[new_fires] $stats$fires_started <- forest$stats$fires_started + length(new_fires)
forest$stats$trees_burned <- forest$stats$trees_burned + length(new_fires)
forest
}
$grid <- grid
forestreturn(forest)
}
The 4th function is for fires to spread. We specify that fires can only spread to adjacent cells if there is a tree already present. We also add that after one iteration a fire is converted to a cooling state (i.e., it can not be a tree or a new fire in the next iteration) to prevent a fire-loop and that a cooling state is converted to an empty state.
# Spread fire to adjacent trees and handle cooling
<- function(forest) {
spread_fire <- forest$grid
grid <- nrow(grid)
size
# Find all fires
<- which(grid == 2, arr.ind = TRUE)
fires <- grid
new_grid
# Spread fire to adjacent trees
if (nrow(fires) > 0) {
for (i in 1:nrow(fires)) {
<- fires[i, 1]
x <- fires[i, 2]
y
# Check adjacent cells (up, down, left, right)
<- rbind(
neighbors c(x - 1, y), c(x + 1, y),
c(x, y - 1), c(x, y + 1)
)
# Only consider valid grid coordinates
<- neighbors[
valid_neighbors 1] > 0 & neighbors[, 1] <= size &
neighbors[, 2] > 0 & neighbors[, 2] <= size,
neighbors[,
]
# Convert adjacent trees to fires
for (n in 1:nrow(valid_neighbors)) {
if (grid[valid_neighbors[n, 1], valid_neighbors[n, 2]] == 1) {
1], valid_neighbors[n, 2]] <- 2
new_grid[valid_neighbors[n, $stats$trees_burned <- forest$stats$trees_burned + 1
forest
}
}
}
}
# Convert fires to cooling state
== 2] <- 3
new_grid[grid # Convert cooling state to empty
== 3] <- 0
new_grid[grid
$grid <- new_grid
forestreturn(forest)
}
We make a simple sidebar layout Shiny app so we can adjust probabilities and see the model change dynamically. We also add some monitoring and print metrics so we can see the number of iterations and the cumulative number of trees grown, fires started, and trees burnt.
# testing ====
# ui ====
<- fluidPage(
ui titlePanel("Forest Fire Model"),
sidebarLayout(
sidebarPanel(
sliderInput("p_growth", "Tree Growth Probability (%)",
min = 0, max = 10, value = 5),
sliderInput("p_fire", "Fire Start Probability (%)",
min = 0, max = 10, value = 1),
actionButton("reset", "Reset Forest"),
hr(),
h4("Statistics:"),
textOutput("iterationCount"),
textOutput("totalTrees"),
textOutput("firesStarted"),
textOutput("treesBurned")
),mainPanel(
plotOutput("forestPlot")
)
)
)
# server ====
<- function(input, output, session) {
server <- 100
size <- reactiveVal(data_init_forest(size))
forest <- reactiveVal(0)
iteration
<- reactiveTimer(200)
autoUpdate
observeEvent(input$reset, {
forest(data_init_forest(size))
iteration(0)
})
observeEvent(autoUpdate(), {
<- forest()
current_forest <- iteration() + 1
iter
# First grow new trees (only in empty, non-cooling cells)
<- grow_trees(current_forest, input$p_growth)
current_forest
# Then start new fires
<- start_fires(current_forest, input$p_fire)
current_forest
# Finally spread existing fires and handle cooling
<- spread_fire(current_forest)
current_forest
forest(current_forest)
iteration(iter)
})
$forestPlot <- renderPlot({
output<- melt(forest()$grid)
forest_data colnames(forest_data) <- c("x", "y", "state")
ggplot(forest_data, aes(x, y, fill = factor(state))) +
geom_tile() +
scale_fill_manual(values = c("black", "green3", "red2", "gray20")) +
guides(fill = "none") +
theme_minimal() +
theme(axis.text = element_blank(),
axis.title = element_blank(),
axis.ticks = element_blank(),
panel.grid = element_blank())
})
# Statistics outputs
$iterationCount <- renderText({
outputpaste("Iteration:", iteration())
})
$totalTrees <- renderText({
outputpaste("Total Trees grown:", forest()$stats$total_trees)
})
$firesStarted <- renderText({
outputpaste("Fires started:", forest()$stats$fires_started)
})
$treesBurned <- renderText({
outputpaste("Trees burned:", forest()$stats$trees_burned)
})
}
# launch ====
shinyApp(ui, server)
This is an iframe
of the rendered app; it takes a while to load and is choppy; it is interactive but mostly its for illustration