Forest Fire Tutorial

Last updated on 16th July 2024

Model overview

In this tutorial we will present an implementation of the forest fire model. This model illustrates how a wild fire spreads through a 2-dimensional grid. Each cell of this grid can either be occupied by a tree or empty. If a tree catches fire, it will ignite its neighboring trees. The model investigates how the fire spread through the environment based on the density of trees (versus empty cells) and the way the fire spreads (i.e. whether it spreads only between nearest neighbor trees or whether it can also affect the next nearest neighbors).

View Online Demo

While modelling of a wild fire might appear as a niche endeavor, this model can be seen as a more general framework for modelling contagion in a system, providing many applications. A direct translation of such model would be to model the spread of a disease in different locations of an environment. However, the model can even provide a template for contagion in a more general sense. For instance, this template could be adapted for modelling the spread of misinformation in a social network.

Summary of the model implementation

The model has the following key components:

  • Cells: each grid cell in the 2-d environment can either be empty or occupied by a tree. The density of trees is hereby a core component of interest with respect to the spread of the fire.
  • Neighborhood environment: each cell is connected to its neighbors. The way cells are connected to each other (e.g. to the nearest neighbors or also the next nearest neighbors), and resulting from this the number for connections, will influence the spread of the fire.
  • The mechanism of spread: in the current model implementation each tree will ignite other trees if they are connected in a neighborhood. However, more sophisticated mechanisms (e.g. probabilistic rules) could be specified to model the mechanism of spread.

Globals

The following global variables are defined in the forest fire model.

ForestFireModel.java

     @Input(name = "gridSize") 1  
     public int gridSize = 50;  
      
      @Input(name = "Empty Cells Proportion")  
            public double emptyCellsPcg = 0.4;  
      
      @Input(name = "Probability of spontaneous ignition")  
            public double probabilityIgnite = 0.01;  
      
      @Input(name = "Neighborhood type: 1 Moore; 2= Von Neumann; 3=customized")  
            public int neighborhood = 2;  
      
      @Input(name = "Neighbors distance")  
            public int similarityThreshold = 2;  
      
     public GridParameters gridParameters;
  • The variable gridSize defines the length of the dimension of the grid environment. For example, a gridSize of 40 will result in a 40*40 environment, i.e. creating 1600 cells.
  • The variable emptyCellsPcg defines the percentage of empty cells overall. For instance, a emptyCellsPcg of 0.4 will result in a 40% empty cells.
  • The variable probabilityIgnite defines the probability that the each tree will ignite spontaneously. In the current model implementation, this spontaneous ignition only happens on the first tick. However, it would be possible to have this event on every tick.
  • The variable neighborhood defines the way in which agents are connected to each other. If equal to 1, the cells will be connected in a Moore neighborhood, which means that agents are connected to their nearest and next nearest neighbors, resulting in 8 neighbors for each cell. If equal to 2, the cells will be connected in a Von Neumann neighborhood, which means that agents are only connected to their nearest neighbor, resulting in 4 neighbors for each cell. Finally, it is possible to design a customized connection strategy where the cells connect to any other cell within a distance. This setting is chosen by assigning the value 3 to the neighborhood variables.
  • The variable similarityThreshold only applies when using a customized connector between cells. In this case, the similarityThreshold defines the furthest distance between two connected cells.
  • The variable gridParameters initializes an object of class GridParameters, which defines the 2-d grid.

Connectors and agent neighborhoods

As explained above, in this model there are different ways to connect the cells/agents. The chosen way of connecting the agents will result in different types of neighborhoods. Importantly, this way of connecting agents will dramatically effect how the fire will spread through the environment. Therefore, the way the environment (or network) is structured is a core component for investigating how the model will behave. Since this is a critical part of the model, we will use this to demonstrate different strategies for forming networks in the SDK.

Importantly, we will also demonstrate how you can build your own customized connectors and connect agents in a flexible way.

Moore Neighborhood

In a Moore neighborhood, each cell is connected to its nearest and next nearest neighbors, i.e. the central cell is connected to its 8 surrounding cells. To connect the cells in a Moore neighborhood, we can use a built-in connector of the SDK in the setup function:

ForestFireModel.java

    @Override  
    public void setup() {  
      Group<ContagionAgent> agentGroup = generateGroup(ContagionAgent.class, getGlobals().gridParameters.nbTotal, agents -> {  
    
    ...
      
     if(getGlobals().neighborhood == 1) {  
            agentGroup.gridConnected(Links.AgentLink.class).mooreConnected();  
      } 
	  

Von Neumann Neighborhood

In a Von Neumann neighborhood, each cell is connected to its nearest neighbors, i.e. the central cell is connected to its 4 adjacent cells. To connect the cells in a Von Neumann neighborhood, we can use a built-in connector of the SDK in the setup function:

ForestFireModel.java

@Override  
public void setup() {  
  Group<ContagionAgent> agentGroup = generateGroup(ContagionAgent.class, getGlobals().gridParameters.nbTotal, agents -> {  

...
  
 if(getGlobals().neighborhood == 2) {  
	agentGroup.gridConnected(Links.AgentLink.class).vonNeumannConnected();
  } 

Customized connector

While the built-in connectors are a great way of easily and efficiently connecting agents, under some circumstance you might wish to design you own custom-made connectors to allow for more flexibility when connecting agents in you simulation. In the context of this forest fire model, one might wish to connect agents based on their location, however, allowing for slightly further distances between agents (e.g. connect to the closest 12 agents instead to the closest 8 agents). Here, it will be demonstrated how to achieve such functionality within the SDK.

Attributes defining similarity

In the 2-d environment, agents are connected if they are in physical proximity. Thus, agents are connected based on their similarity with respect to their x- and y-coordinate in the grid. Importantly, this is a special case of a very general principle of homophily. Homophily describes the finding that agents are more likely to connect if they are similar in some attributes, and this principle applies to a wide range of situations such as social networks where people are more likely to connect to each other if they share similar views. Therefore, connecting agents based on their location should be seen as an illustration and can easily be applied to other attributes. First, each agent needs to be assigned some attributes based on which they can be connected. This happens in the setup() function where the agents are initialized.

ForestFireModel.java

    @Override  
    public void setup() {  
        getGlobals().gridParameters = new GridParameters(gridSize2, getGlobals().emptyCellsPcg);  

      Group<ContagionAgent> agentGroup = generateGroup(ContagionAgent.class, getGlobals().gridParameters.nbTotal, agents -> {  
            Cell newCell = getGlobals().gridParameters.assignCell();  
		    if(getGlobals().neighborhood != 1 && getGlobals().neighborhood != 2) {  
                agents.attribute1 = newCell.getXCoordinate();  
			    agents.attribute2 = newCell.getYCoordinate();  
      }  
       agents.initializeState(newCell.isEmpty());  
      agents.frequencyIgnition = getGlobals().probabilityIgnite;  
      });

Each agent is initialized with their x- and y-coordinates as attribute. These coordinates are defined in the gridParameters class as follows:

GridParameter.java

    public class GridParameters {  
     public int nbTotal;  
     public int nbOccupied;  
     public int nbEmpty;  
     public ArrayList<Integer> yCoordinates = new ArrayList<Integer>();  
     public ArrayList<Integer> xCoordinates = new ArrayList<Integer>();  
     public ArrayList<Boolean> stateList = new ArrayList<>();  
     public ArrayList<Cell> cellList = new ArrayList<Cell>();  
      
     public GridParameters(int gridSize, double emptyCellsPcg) {  
	     nbTotal = (int) Math.pow(gridSize, 2);  
	     nbEmpty = (int) Math.ceil(nbTotal * emptyCellsPcg);  
	     nbOccupied = nbTotal - nbEmpty;  
	     int counter = 0;  
      
	     for (int i = 0; i < gridSize; ++i) {  
                for (int j = 0; j < gridSize; ++j) {  
                    xCoordinates.add(i);  
				    yCoordinates.add(j);  
					     if(counter < nbOccupied) {  
	                        stateList.add(false);  
					      } else {  
	                        stateList.add(true);  
					      }  
                    counter++;  
			      }  
		     }	  
      
	     Collections.shuffle(stateList);  
	     for (int k =0; k < nbTotal; k++){  
                    Cell newCell= new Cell((int) xCoordinates.get(k), (int) yCoordinates.get(k), stateList.get(k));  
	      cellList.add(newCell);  
	      }  
     }  
      
    public Cell assignCell(){  
      Cell outputCell = cellList.get(0);  
      cellList.remove(0);  
     return outputCell;  
      }  
    }

Each cell has now two attributes, defining their x- and y-coordinate.

Start with a fully connected network

To define your customized network, we start with a fully connected network and later prune this network based on the (dis)similarity between agents. Therefore, in the setup() function, a fully connected network is initialized:

ForestFireModel.java

    @Override  
    public void setup() {  
      Group<ContagionAgent> agentGroup = generateGroup(ContagionAgent.class, getGlobals().gridParameters.nbTotal, 
      if(getGlobals().neighborhood == 3) {  
		agentGroup.fullyConnected(agentGroup, Links.AgentLink.class);
      } 

Prune the network

On the first tick of the simulation, the network needs to be pruned. This happens with the following commands in the step() function:

ForestFireModel.java

if (getContext().getTick() == 0) {  
    if(getGlobals().neighborhood !=1 && getGlobals().neighborhood != 2){  
        run(ContagionAgent.informNeighborAttribute(), ContagionAgent.pruneConnection());  
    }
}

There are two relevant component here. First, each agent sends a message to all other agents, informing them about its attributes (i.e. its x- and y-coordinate). Second, each agent takes the information received from the other agents to prune their network.

ContagionAgent.java

    private void sendAttribute(int attribute1, int attribute2){  
        getLinks(Links.AgentLink.class).send(Messages.AttributeMessage.class, (msg, link) -> {  
            msg.attribute1=attribute1;  
      msg.attribute2=attribute2;  
      });  
    }

Here, each agent sends a message with its attributes (in this case x- and y-coordinate) to all other agents.

ContagionAgent.java

    public static Action<ContagionAgent> pruneConnection() {  
        return Action.create(  
                ContagionAgent.class, contagionAgent ->  
                        contagionAgent.getMessagesOfType(Messages.AttributeMessage.class).forEach(msg -> {  
                            double dissimilarity = Math.pow((msg.attribute1 - contagionAgent.attribute1), 2) + 		    Math.pow((msg.attribute2 - contagionAgent.attribute2), 2);  
						    if (dissimilarity > contagionAgent.getGlobals().similarityThreshold) {  
                                contagionAgent.removeLinksTo(msg.getSender());  
						      }  
                        }));  
    }

Each agent receives a message from all other agents, stating their attributes. Based on this, the agent calculates a dissimilarity (in this case a distance) to each other agent. If the other agent is further away than a predefined threshold, the link to this agent is removed. Through this process only the links between neighboring agents remain. Importantly, a similar pruning process can be applied to other attributes, enabling to create neighborhoods based on similarity of any attribute.

Spreading contagion

After having established a network structure, we are interested in how the fire spreads through the environment. On the first step, some trees will spontaneously catch fire (e.g. due to a lightning bolt). This creates the starting condition for the spread of fire:

ForestFire.java

    public void step() {  
        super.step();  
     if (getContext().getTick() == 0) {  
     
     ...
     
         run(ContagionAgent.spontaneousIgnite());  
	     return;  
      }

ContagionAgent.java

    public static Action<ContagionAgent> spontaneousIgnite() {  
        return Action.create(  
                ContagionAgent.class, contagionAgent -> {  
                    double igniteSpontaneous = Math.random();  
				    if (igniteSpontaneous < contagionAgent.frequencyIgnition) {  
	                        contagionAgent.switchToInfected();  
	                         }  
                });  
    }

Hereby, each tree will be ignited with a predefined probability (e.g. 1%). After the initial ignition, the burning trees will spread fire to their neighbors:

ContagionAgent.java

    public static Action<ContagionAgent> sendContagion() {  
        return Action.create(  
                ContagionAgent.class, contagionAgent -> {  
                    if(contagionAgent.getState() == State.INFECTED) {  
                        contagionAgent.getLinks(Links.AgentLink.class).send(Messages.SendContagion.class);  
					    contagionAgent.switchToRecovered();  
				      }  
                });  
    }

After spreading the fire to its neighbors, the each tree switch their status to BURNED indicating that this tree won't be susceptible for catching fire in the future. Finally, each tree that has been ignited switches their status to BURNING:

    public static Action<ContagionAgent> receiveContagion() {  
        return Action.create(  
                ContagionAgent.class, contagionAgent ->  
                    contagionAgent.getMessagesOfType(ForestFire.Messages.SendContagion.class).forEach(msg ->  
                        contagionAgent.switchToInfected()));  
    }

In this implementation, the transmission of fire is deterministic (i.e. every neighbor of a burning tree will catch fire), however, it would be easy to include a more sophisticated mechanism here for transmission (e.g. a probabilistic function).

Visualizing the output in the console

The model can be easily visualized in the console. Going to the Network view and stepping through the ticks will show you how the fire spreads through the network. This is a very simple way of sense checking whether the model behaves in predicted ways. console forestFire

This is an easy and convenient way of looking at the model output.

Data output

While it is convenient to look at the model output in the console, this is less flexible and does not allow further, more sophisticated, analysis of the model output. Therefore, it can often been useful to output the data and pull it into some analysis tool (e.g. a Jupyer Notebook) to visualize the data and conduct further analysis.

In order to save and output data, we will use the build-in parquet writers. This will also provide us with an opportunity to illustrate how these data output channels work.

First, we need to initialize the output channels, in the init() function, basically defining a channel to utilize the built-in functions. For this purpose, a schema needs to be defined, illustrating the kind of data that will be used as output in this channel:

ForestFireModel.java

    getContext()  
            .getChannels()  
            .createOutputChannel()  
            .setId("contagion_output")  
            .setSchema(Monitor.getMonitorSchema())  
            .addLabel("simudyne:parquet")  
            .build();
  • setId() -> A unique string used to identify the data channel.
  • setSchema() -> The schema defines the type of data you will be exporting with this channel.

In the following we will showcase how to define a schema. Here we create a separate class called Monitor to define the schema. For our purposes we aim to export each agents ID and their State:

Monitor.java

        public class Monitor {  
        public long agentID;  
        public State state; 
        
        public Monitor(long agentID, State state) {  
        this.agentID = agentID;  
        this.state = state;  
       }
       public static SchemaRecord getMonitorSchema() {  
                return new SchemaRecord("monitor")  
                        .add(new SchemaField("agentID", FieldType.Long))  
                        .add(new SchemaEnum("agent_status", Arrays.asList("EMPTY",  
          "HEALTHY",  
          "INFECTED",  
          "RESISTANT")));  
          }
        public ValueRecord getMonitorValue() {  
    		    return new ValueRecord("monitor")  
    	            .addField("agentID", this.agentID)  
    		           .addField("agent_status", this.state);  
    		           }
       }

In the function getMonitorSchema() a general schema is set-up, which is used in the initialization of the model. In comparison, getMonitorValue() is used to actually assign data to the schema.

Finally, the schema data needs to be written to the output channel. In this example, each agent assigns its state to the channel with the following action:

ContagionAgent.java

    public static Action<ContagionAgent> writeData() {  
        return Action.create(  
                ContagionAgent.class, contagionAgent -> {  
                    Monitor agentMonitor = new Monitor(contagionAgent.getID(), contagionAgent.state);  
				    ValueRecord agentOutput = agentMonitor.getMonitorValue();  
				    contagionAgent.getContext().getChannels().getOutputChannelWriterById("contagion_output").write(agentOutput);  
			      });  
    }

Here, the agents ID and State are translated into the schema format and then written to the data channel.

Through calling the writeData() function on each tick, the data for each agent and each tick is appended to the parquet file.

ForestFireModel.java

        public void step() {  
            super.step();
    
    ....
    
            run(ContagionAgent.writeData());
        }

Analyzing data output in Jupyter Notebook

After having created a data output as parquet file, it is easy to load the data into any preferred data analysis tool. Here, we will use a Jupyter Notebook. The parquet file can easily be imported and read into a pandas data frame, using pd.read_parquet(). This will lead to a data table with a row for each agent at each tick. This data can then be transformed back into a grid format and animated with Matplotlib:

    import pandas as pd
    import pyarrow
    import numpy as np
    import math
    import matplotlib.pyplot as plt
    import matplotlib
    from IPython.display import HTML
    import matplotlib.patches as mpatches
    import matplotlib.animation as animation
        
    data = pd.read_parquet(r"C:\Users\mrollwage_smd\OneDrive - Simudyne Limited\Documents\Python Scripts\ForestFireModel_Analyse\run000000000.parquet")
    ticks= data['tick'].iloc[-1]
    data_timepointZero= data[data['tick']==1]
    dimensions=int(math.sqrt(len(data_timepointZero)))
    timePointGrid_full=np.array([[[0 for k in range(dimensions)] for j in range(dimensions)]for j in range(ticks)])
    
    for tick in range(1, ticks): 
        data_timepoint= data[data['tick']==tick]
        timePointGrid=np.array([[0 for k in range(dimensions)] for j in range(dimensions)])
        for cell in range(len(data_timepoint)): 
        row = int(data_timepoint['agentID'].iloc[cell]%dimensions)
            column = int(data_timepoint['agentID'].iloc[cell]/dimensions)
            value=-99
            if data_timepoint['agent_status'].iloc[cell] == b'EMPTY': 
                value=-1
            elif data_timepoint['agent_status'].iloc[cell] == b'HEALTHY': 
                    value =0
            elif data_timepoint['agent_status'].iloc[cell] == b'INFECTED': 
                    value =1
            elif data_timepoint['agent_status'].iloc[cell] == b'RESISTANT': 
                    value =2
            timePointGrid[column, row]=value
            timePointGrid_full[tick, column, row]=value


    dim1=timePointGrid_full.shape
    values = np.unique(timePointGrid_full.ravel())
    print(values)
    fig=plt.figure(figsize=(8, 8))
    cmap = matplotlib.colors.ListedColormap(['lightgrey', 'green', 'red', 'black'])
    grid =plt.imshow(timePointGrid_full[1,:,:], cmap=cmap, interpolation='none')
    colors = [grid.cmap(grid.norm(value)) for value in values]
    patches = [mpatches.Patch(color=colors[0], label="empty"), mpatches.Patch(color=colors[1], label="healthy") , mpatches.Patch(color=colors[2], label="burning"), mpatches.Patch(color=colors[3], label="burned")]
    plt.legend(handles=patches, bbox_to_anchor=(0.8, 0),
              ncol=4, fancybox=True, shadow=True)
    plt.grid(True)
    plt.axis('off')
    
    def animate_noise(i):
        grid.set_array(timePointGrid_full[i,:,:])
        return [grid]
    anim = animation.FuncAnimation(fig, animate_noise, frames=dim1[0], interval=500, blit=True)
    plt.close(fig)
    
    HTML(anim.to_html5_video())

Summary

In this tutorial we have walked through an implementation of the forest fire model. Besides the specific topic of the model, this can be used as template for modelling contagion in a system, enabling multiple applications. Besides the specific model, we also illustrated how to create customized connectors to create flexible rules for forming connections between agents. Moreover, we also showcased how to export data using the parquet writers and import this data into a Jupyter Notebook for further analysis and visualization.