1
0
Fork 0

Add documentation and supporting files

This commit is contained in:
Rory Healy 2023-02-05 20:10:16 +11:00
parent 44343e8b80
commit 2b115ce3e7
Signed by: roryhealy
GPG Key ID: 610ED1098B8F435B
9 changed files with 487 additions and 0 deletions

8
data/bushfire-0.txt Normal file
View File

@ -0,0 +1,8 @@
2
2,2
0,2
1,2
1,2
1
N
0,0

10
data/bushfire-1.txt Normal file
View File

@ -0,0 +1,10 @@
3
1,2,1
0,1,2
0,0,1
1,1,1
2,1,2
3,2,2
1
N
1,1

469
docs/SPECIFICATION.md Normal file
View File

@ -0,0 +1,469 @@
# 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](../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](../data/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('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('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](../images/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](../images/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](../images/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](../images/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](../images/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)]

BIN
images/bushfire_model.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
images/wind_example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB