1
0
Fork 0
simple-bushfire-model/docs/SPECIFICATION.md

23 KiB
Raw Permalink Blame History

Assignment Specification

Below is the assignment specification, in full, slightly edited for context and appearence.

Introduction

Computers can be used to build models of complex real world systems such as the weather, traffic and stockmarkets. Computer models can help us to understand the dynamics of these real world systems, and to make decisions about them. For example, a model that can predict Saturday's weather could help us decid whether to go to the beach or not. A traffic model could help us plan where to build a new road to reduce traffic jams. A model of the stockmarket could help us decide when to buy and sell shares.

In this project, you will build a simple model of a bushfire spreading across a landscape represented as a two-dimensional grid. The speed and direction that a bushfire will spread in is affected by many factors, including the amount and type of fuel (trees) available, the slope of the land, and the direction of the wind. Your model will help forecast how much damage (number of grid squares burnt) a fire will do before it burns out.

Models like this (but much more complicated!) are used to help predict which way a fire might spread, to assess where high risk fires are most likely to occur, and help understand how the damage cause by fires can be reduced via management practices. See Phoenix rapid fire for an example of a much more complex model (developed at the University of Melbourne).

In this project we will look at three different problems related to a bushfire

  • How to build a bushfire model from a data file.
  • How to determine if a cell in a specific location will ignite.
  • How to model the spread of a bushfire.

You will also need to write and submit a set of test cases for evaluating the model.

The bushfire model

We model a bushfire as occurring in a square grid made up of M by M cells. The cell (i, j) refers to the cell in the ith row and the jth column of the grid. Each cell typically has eight adjacent cells (horizontally, vertically and diagonally). Cells on the edge of the grid are adjacent to only five other cells; cells in the corners of the grid are adjacent to only three other cells. For convenience, we take the top of the grid to be North, the right side to East, the bottom to be South and the left side to be West. See images/bushfire_model.png for reference.

Each cell in the M by M grid has several attributes:

  • fuel load: the amount of combustible material (trees, leaf litter, etc) in that cell. The fuel load of a cell is represented as a non-negative integer, where 0 equates to no combustible material.
  • height: the elevation of the cell. Height is represented as a positive integer (a cell with height 3 is higher than a cell with height 2, which is in turn higher than a cell with height 1, and so on). Fires will tend to spread more rapidly uphill, and more slowly downhill, compared to flat ground.
  • burning: a Boolean value indicating whether the cell is currently burning or not. A cell can only be burning if it's fuel load is greater than 0.

In addition, the entire grid is characterised by three further attributes:

  • ignition threshold: how combustible the landscape is (for instance, a dry landscape would be more combustible than a wet landscape, and have a lower ignition threshold). The ignition threshold is represented by an integer greater than 0. A non-burning cell will start burning if its ignition factor (explained below) is greater than or equal to the ignition threshold.
  • ignition factor: the intensity of fire around a particular non-burning cell at a given point in time that, in combination with the ignition threshold described above, will be used to determine whether a cell will start burning. The ignition factor is floating point number; details of how to calculate the ignition factor are provided in the following sections.
  • wind direction: if a wind is blowing, it can carry embers that allow a fire to spread more rapidly in a particular direction. Wind may blow from one fire to directions: North, Northeast, East, Southeast, South, Southwest, West or Northwest (abbreviated N, NE, E, SE, S, SW, W, NW) or there may be no wind. How the wind affects ignition will be explained further.

Part 1 - Parse a bushfire scenario file

Your task is to write a function parse_scenario(filename) that parses a file with the structure described below, validates the contents and returns either a dictionary containing all values required to specify a model scenario if the contents are valid, or None if any of the contents are invalid.

The structure of the file is as follows:

  • an integer specifying the width and height (M) of the square landscape grid;
  • M lines, each containing M integer values (separated by commas), defining the initial fuel load of the landscape (a visual representation of the M by M grid);
  • M lines, each containing M integer values (separated by commas), defining the height of the landscape (again, a visual representation of the M by M grid);
  • an integer specifying the ignition threshold for this scenario;
  • a one or two character string specifying the wind direction for this scenario (or None if there is no wind); and
  • one or more lines, each containing the (i, j) coordinates (row number, column number) of a cell which is burning at the start of the simulation.

For example, the file bushfire-0.txt specifies a 2 by 2 landscape:

  • there is an initial fuel load of 2 in three of the four cells, with a fuel load of 0 in the cell (1, 0);
  • the height of all cells in the first column (j = 0) is 1, while the height of all cells in the second column (j = 1) is 2 (i.e., the landscape slopes up toward the East);
  • the dimensions of the grid are a positive integer;
  • the ignition threshold is a positive integer not greater than eight;
  • the wind direction is valid; and
  • the coordinates of the burning cells are (a) located on the landscape, and (b) have non-zero initial fuel load.

If all values are valid, your function should return a dictionary with key/value pairs as follows:

  • f_grid: a list of lists (of dimensions M by M);
  • h_grid: a list of lists (of dimensions M by M);
  • i_threshold: an integer;
  • w_direction: a string or None; and
  • burn_seeds: a list of tuples.

If there is no wind, w_direction can be either an empty string '' or None. If any values are invalid, your function should return None.

For example:

>>> parse_scenario('data/bushfire-0.txt')

{'f_grid': [[2, 2], [0, 2]], 'h_grid': [[1, 2], [1, 2]], 'i_threshold': 1, 'w_direction': 'N', 'burn_seeds': [(0, 0)]}

>>> parse_scenario('data/bushfire-1.txt')

{'f_grid': [[1, 2, 1], [0, 1, 2], [0, 0, 1]], 'h_grid': [[1, 1, 1], [2, 1, 2], [3, 2, 2]], 'i_threshold': 1, 'w_direction': 'N', 'burn_seeds': [(1, 1)]}

Updating the model

The state of the bushfire model is defined by the attributes (fuel load, height, burning, ignition threshold and wind direction) described in a previous section. Of these, fuel load and burning status may change over time as the bushfire spreads. Height, ignition threshold and wind direction are fixed and never change.

The state of the model is updated in discrete timesteps, t = 0, 1, 2, ..., and at each timestep, the following things happen:

  • If a cell is currently burning, it's fuel load will be reduced by one in the following timestep. If this will result in its fuel load reaching zero, it will stop burning in the following timestep.
  • If a cell is not currently burning, it may start burning, depending on its proximity to other cells which are burning in this timestep. The rules describing how to determine if a cell catches fire are described in the next section.

Note, the future state of each cell in time t + 1 is determined on the basis of the current state of the grid at time t. We stop updating the model when no cells are burning, as the state will not change after this point.

Determining when a cell catches fire (simple)

To determine whether a cell (i, j) catches fire, we calculate its ignition factor and compare this to the landscape's ignition threshold. As stated above, a cell will catch fire if its ignition factor is greater than or equal to the ignition threshold. A cell must be currently not burning and have a fuel load greater than 0 in order to catch fire.

We will start by considering the simplest case: a flat landscape with no wind in which cell (i, j) is not currently burning. In this case, each cell adjacent to (i, j) that is burning contributes 1 point to (i, j)'s ignition factor. Thus, if the ignition threshold is 1, (i, j) will start burning if it is adjacent to at least one burning cell. If the ignition threshold is 2, (i, j) will start burning only if it is adjacent to at least two burning cells.

Consider the sequence of landscape states shown in simple_bushfire_model.png, in which all cells are of the same height, there is no wind, and the ignition threshold is 1. At the first timestep (t = 0), the two cells (0, 0) and (0, 1) are burning. During this timestep, the fuel load of each of these two cells will decrease by one (causing the fire in (0, 0) to stop burning in the next time step, t = 1). If we then consider at the surrounding cells, (1, 0) has a fuel load of 0, so it cannot catch fire. Cells (0, 2), (1, 1) and (1, 2) each have a fuel load greater than 0 and are adjacent to one of the currently burning cells, (0, 1), therefore they will start burning in the next time step (t = 1).

The example in simple_bushfire_model_2.png is identical to that in the previous example, but the ignition threshold is now 2, meaning that each non-burning cell needs to be adjacent to two or more burning cells in order to start burning. Therefore, cells (0, 2) and cells (1, 2) will not start burning at t = 1, as they were only adjacent to a single burning cell at t = 0.

Only cells that are located within the bounds of the landscape grid can contribute to a cell's ignition factor. Thus, a cell located in the corner of a grid will, in this simple case, only have three adjacent cells that may cause it to start burning.

Determining when a cell catches fire (height included)

Height and wind modify can modify the calculation of the basic ignition factor described in the previous section. The effect of landscape height is described in this section.

On a flat landscape, where neighbouring cells are of the same height, each burning cell contributes 1 point to an adjacent non-burning cell's ignition factor.

However, bushfires tend to spread more rapidly uphill, therefore if a cell (i, j) has height that is greater than that of an adjacent burning cell, that cell will contribute twice as much (i.e., 2 points) to (i, j)'s ignition factor. Conversely, bushfires tend to spread more slowly downhill, therefore an adjacent burning cell with height greater than (i, j)'s height will contribute only half as much (i.e., 0.5 points) to (i, j)'s ignition factor.

In the sequence shown in height_bushfire_model.png, the height of each cell is indicated by a small blue number. No wind is blowing and the ignition threshold of the landscape is 2. At the first timestep (t = 0), a single cell (0, 0) is burning. The cell to the South of it (1, 0) has a fuel load of 0 and hence cannot catch fire. On a flat landscape, the other two adjacent cells (0, 1) and (1, 1) would not catch fire either, as their ignition factor would only be 1 (from the single burning cell (0, 0)), below the ignition threshold of 2. However, in this landscape, cells (0, 1) and (1, 1) are higher than cell (0, 0), therefore its contribution to their ignition factor is doubled to 2 points, high enough to equal the landscape's ignition threshold and cause them to start burning in the following timestep (t = 1).

In constrast, the top-right cell (0, 2) will escape being burnt in timestep t = 2 (and beyond) as it is lower than the surrounding cells, hence they each contribute only 0.5 points to its ignition factor. Thus, even when all three surrounding cells are burning, (0, 2)'s ignition factor is only 1.5, below the landscape ignition threshold of 2.

Determining when a cell catches fire (wind included)

Wind can carry burning embers that allow a fire to spread more rapidly in a particular direction. If a wind is blowing, up to three additional cells are considered to be adjacent to (i, j) for the purpose of calculating its ignition factor.

For example, as shown in wind_example.png, if a wind is blowing from the North, the cell two cells above (i, j) and the cells immediately to the left and right of this cell are considered adjacent to (i, j) (i.e., cells (i 2, j 1), (i 2, j) and (i 2, j + 1)). If any of these additional cells are burning, they will also contribute when calculating (i, j)'s ignition factor.

If a wind is blowing from the Southwest, the cell two cells below and to the left of (i, j) and the cells immediately above and to the right of this cell are considered adjacent to (i, j). That is, cells (i + 1, j 2), (i + 2, j 2) and (i + 2, j 1). Of course, these additional cells must be within the bounds of the landscape in order to have any effect, as in the simple case in the previous section.

The sequence shown in wind_bushfire_model.png is identical to the simple example shown in the previous section (on a flat landscape, with ignition threshold of 2) except that the wind is now blowing from the Northwest. As a consequence, cell (0, 0) is considered adjacent to (1, 2), which therefore has an ignition factor of 2 (as (0, 1) is also adjacent and burning) and hence will start burning at t = 1. In addition, (0, 0) and (0, 1) are also both considered adjacent to (2, 2), which will also start burning at t = 1. Bushfires spread much more rapidly when the wind is blowing!

When considering the joint effects of height and wind, you should compare the heights of (i, j) and each of its adjacent cells on a pairwise basis, disregarding the heights of any other surrounding cells. For example, if (0, 0) was higher than (2, 2) and (0, 1) was lower than (2, 2) then they would contribute 0.5 points and 2 points respectively to (2, 2)'s ignition factor, irrespective of the heights of the intervening cells (e.g., (1, 1)).

Part 2 - Determine if a cell starts burning

Based on the rules described earlier, your task is to write a function check_ignition(b_grid, f_grid, h_grid, i_threshold, w_direction, i, j) that takes as arguments the burning state b_grid (at time t), current fuel load f_grid (at time t), height h_grid, ignition threshold i_threshold, wind direction w_direction and coordinates i and j of a cell, and returns True if that cell will catch fire at time t + 1 and False otherwise.

The arguments are of the following types:

  • b_grid: a list of lists of Boolean values (of dimensions M by M)
  • f_grid: a list of lists of integers (of dimensions M by M)
  • h_grid: a list of lists of integers (of dimensions M by M)
  • i_threshold: an integer
  • w_direction: a string (if wind is blowing), otherwise None (if no wind is blowing)
  • i and j: integers (i, j < M)

You may assume that all arguments are valid, as defined in Part 1.

For example:

>>> check_ignition([[True, False], [False, False]], [[2, 2], [2, 2]], [[1, 1], [1, 1]], 1, 'N', 0, 1)

True

>>> check_ignition([[True, False], [False, False]], [[2, 0], [2, 2]], [[1, 1], [1, 1]], 1, 'N', 1, 0)

True

>>> check_ignition([[True, True, False], [False, False, False], [False, False, False]], [[1, 1, 1], [1, 1, 1], [1, 0, 0]], [[2, 2, 1], [2, 3, 1], [1, 1, 1]], 1, None, 0, 2)

False

>>> check_ignition([[True, True, False], [False, False, False], [False, False, False]], [[1, 1, 1], [1, 1, 1], [1, 0, 0]], [[2, 2, 1], [2, 3, 1], [1, 1, 1]], 2, None, 1, 1)

True

Part 3 - Run the model

Your task is to write a function run_model(f_grid, h_grid, i_threshold, w_direction, burn_seeds) that takes as arguments the initial fuel load f_grid (i.e., at time t = 0), height h_grid, ignition threshold i_threshold, wind direction w_direction and a list of cells burn_seeds that are burning at time t = 0, and returns a tuple containing (a) the final state of the landscape once the fire has stopped burning, and (b) the total number of cells that have been burnt by the fire (including any initially burning cells in burn_seeds).

The arguments are of the following types:

  • f_grid: a list of lists (of dimensions M by M)
  • h_grid: a list of lists (of dimensions M by M)
  • i_threshold: an integer
  • w_direction: a string
  • burn_seeds: a list of integer tuples (i, j) where i, j < M

You may assume that all arguments are valid, as defined in previous parts.

You may find it helpful to define one or more additional functions that carry out a single step of the model run, determining the new burning state and fuel load at time t + 1 on the basis of the model state at time t.

For example:

>>> run_model([[2, 2], [2, 2]], [[1, 1], [1, 1]], 1, 'N', [(0, 0)])

([[0, 0], [0, 0]], 4)

>>> run_model([[2, 0], [0, 2]], [[1, 1], [1, 1]], 2, 'S', [(0, 0)])

([[0, 0], [0, 2]], 1)

Part 4 - Test cases

In addition to implementing your solutions, you are also required to submit a set of test cases that can be used to test your Part 3 run_model function.

You should aim to make your test cases as complete as possible. That is, they should be sufficient to pick up incorrect implementations of the model.

Your set of test cases may assume that the input passed to your function will be of the correct types and will be well-formed.

Your test cases suite will be evaluated by running it on several known incorrect implementations, in which it should detect incorrect behaviour; i.e., returning an incorrect final state of the landscape and/or number of cells burnt.

You should specify your test cases as a series of calls to the function test_run_model(run_model, input_args, expected_return_value), where the first argument run_model is the model you created in Part 3, the second argument, input_args contains a list of f_grid, h_grid, i_threshold, w_direction, burn_seeds representing the call to the function run_model (described in Part 3) and expected_return_value is the expected return from the function run_model, namely a list containing the final state of the landscape once the fire has stopped burning, and the total number of cells that have been burnt by the fire as in Part 3).

test_run_model will return True if the model successfully passes the test, and False otherwise.

For example, using the first two examples from Part 2:

>>> test_run_model(run_model, [[[2, 2], [2, 2]], [[1, 1], [1, 1]], 1, 'N', [(0, 0)]],
    [[[0, 0], [0, 0]], 4])

True

>>> test_run_model(run_model, [[[2, 0], [0, 2]], [[1, 1], [1, 1]], 2, 'S', [(0, 0)]],
    [[[0, 0], [0, 2]], 1])

True

Part 5 - Bonus

The final part is for bonus marks, and is deliberately quite a bit harder than the four basic questions (and the number of marks on offer is deliberately not commensurate with the amount of effort required — bonus marks aren't meant to be easy to get!). Only attempt this is you have completed the earlier questions, and are up for a challenge!

In this question, you will use the bushfire model to determine the optimal cell or cells in a landscape in which to conduct a prescribed burn in order to best protect a town from a future bushfire of unknown timing and origin.

For this question, we modify our original definition of a landscape to include a town cell, containing a town. The town cell has a non-zero fuel load; that is, the town can catch fire.

Prescribed burns

A prescribed burn is a controlled fire used as part of forest management in order to reduce the risk of future uncontrolled fires.

In our simulation model, the rules of a prescribed burn are that it will only occur on a day with no wind, and will commence on a single prescribed burn cell with a non-zero fuel load. A prescribed burn will not be conducted on the cell containing the town.

A prescribed burn spreads just like a normal bushfire; however, due to the controlled nature of the fire, any burning cell contributes only half as many points to the ignition factor of adjacent cells as it ordinarily would (taking slope into account). That is, if it would normally contribute 0.5, 1 or 2 points, it will now only contribute 0.25, 0.5, or 1 points. This reduction applies both to the original prescribed burn cell and to any cell that subsequently catches fire during the prescribed burn. As with a normal bushfire, a prescribed burn will continue until no cells remain on fire.

Scoring prescribed burn cells

We filter out invalid prescribed burn cells and score the remaining valid prescribed burn cells as follows:

  • Any prescribed burn cell that results in the the town cell catching fire is deemed invalid.
  • Following the completion of a prescribed burn, we will consider scenarios in which potential bushfires start in any (single) seed cell with a non-zero fuel load (after the prescribed burn), except for the town cell, on a day with any possible wind conditions. Thus, for a landscape of dimensions M, we will consider up to 9 × (M^2 2) bushfire scenarios. 2 is subtracted because we don't seed a bushfire on the town cell or cells with zero fuel load, of which there is at least one, being the cell in which the prescribed burn was conducted. For each seed cell there are 9 possible wind directions to consider, including no wind.
  • Valid prescribed burn cells are scored according to the proportion of scenarios in which the town cell caught fire.

The optimal cell or cells for prescribed burning are those with the lowest score; that is, that have been more effective at protecting the town.

In the first example below, there are 4 cells with a non-zero fuel load, one of which (1, 1) is the town cell. Therefore there are three cells in which a prescribed burn can be conducted. None of these will result in the town being burnt, therefore they are all valid. When we test the 18 possible bushfire scenarios, we find that for one valid prescribed burn cell (0, 1), all subsequent bushfires will result in the town catching fire. For the other two prescribed burn cells ((0, 0) and (1, 0)), only half of the subsequent bushfires will result in the town catching fire; thus, either of these would be the optimal location in which to carry out a prescribed burn in this landscape.

Your task is to write a function plan_burn(f_grid, h_grid, i_threshold, town_cell) that determines the optimal prescribed burn cell or cells. f_grid, h_grid and i_threshold are all as defined in Parts 2 and 3. town_cell is a tuple containing the coordinates of the town cell.

Your function should return a sorted list containing the coordinates of the optimal prescribed burn cell or cells, as defined above. If there are no valid prescribed burn cells, this list will be empty.

For example:

>>> plan_burn([[2, 2], [1, 2]], [[1, 2], [1, 2]], 2, (1, 1))

[(0, 0), (1, 0)]

>>> plan_burn([[0, 0, 0, 0, 0], [0, 2, 2, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 1, 0], [1, 0, 0, 0, 0]], [[2, 2, 2, 2, 2], [2, 1, 2, 2, 2], [2, 2, 2, 2, 2], [2, 2, 2, 1, 2], [2, 2, 2, 2, 2]], 2, (3, 3))

[(1, 1), (1, 2), (2, 3)]