Initial commit. Note - this is the original assignment submission. No modifications have been made.

This commit is contained in:
Rory Healy 2024-06-08 21:17:07 +10:00
commit f4235d6e3c
Signed by: roryhealy
GPG key ID: 0A3CBDE9C2AE672F
16 changed files with 1324 additions and 0 deletions

10
bf.dat 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

8
bf0.dat Normal file
View file

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

BIN
bushfire_model.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
height_bushfire_model.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

75
part1.py Normal file
View file

@ -0,0 +1,75 @@
# Title: Project 2 - Parse a bushfire scenario file
# Author: Rory Healy
# Date created - 8th May 2019
# Date modified - 9th May 2019
def strlist_to_listlist(grid):
'''Converts a list of strings into a list of lists.'''
for i in range(len(grid)):
line_to_append = []
for j in range(len(grid[i])):
current_coordinate = grid[i][j]
if current_coordinate not in ",":
line_to_append.append(int(grid[i][j]))
grid[i] = line_to_append
return grid
def parse_scenario(filename):
'''Parses a file with the structure described, validates the contents and
returns a dictionary containing all values that are required to model a
scenario, or None if any of the contents are invalid.'''
structure_dict = {}
# Converts the file to a list of strings.
structure = open(filename, "r")
all_lines = []
for line in structure.readlines():
line = line[:-1]
all_lines.append(line)
# Finds the length of the matrix and splits the data up acccordingly.
# e.g. a M*M matrix will store f_grid and h_grid as a list of M lists.
# This is used to return the f_grid and h_grid in the correct format.
matrix_size = int(all_lines[0])
if matrix_size <= 0:
return None
f_grid = []
h_grid = []
for i in range(1, matrix_size + 1):
f_grid.append(all_lines[i])
for i in range(matrix_size + 1, matrix_size * 2 + 1):
h_grid.append(all_lines[i])
strlist_to_listlist(f_grid)
strlist_to_listlist(h_grid)
# Remove matrix_size, f_grid and h_grid from all_lines and define
# i_threshold, w_direction and burn_seeds so that they can be added to
# structure_dict.
all_lines = all_lines[((matrix_size * 2) + 1):]
i_threshold = int(all_lines[0])
w_direction = '' + all_lines[1]
if all_lines[2]:
burn_seeds = []
burn_seeds.append(all_lines[2])
burn_seeds = [(int(burn_seeds[0][0]), int(burn_seeds[0][-1]))]
# Checks if the values are valid.
if i_threshold > 8 or i_threshold < 0:
return None
for i in range(len(burn_seeds)):
current_seeds = list(burn_seeds[i])
for j in range(len(current_seeds)):
if current_seeds[j] > matrix_size:
return None
# Fills in structure_dict with the keys and associated values.
all_dict_keys = ['f_grid', 'h_grid', 'i_threshold', 'w_direction',
'burn_seeds']
all_dict_values = [f_grid, h_grid, i_threshold, w_direction, burn_seeds]
for i in range(len(all_dict_values)):
structure_dict[all_dict_keys[i]] = all_dict_values[i]
structure.close()
return structure_dict

225
part2.py Normal file
View file

@ -0,0 +1,225 @@
# Title: Project 2 - Determine if a cell starts burning
# Author: Rory Healy
# Date created - 9th May 2019
def get_adjacent_cells(b_grid, wind):
'''Returns a list of cells that are considered adjacent to the cells that
are currently burning.'''
# Note: all_adjacent_cells includes cells that aren't in all_cells_list.
# Hence, adjacent_cells_list is returned once these invalid cells have
# been removed.
all_adjacent_cells = []
adjacent_cells_list = []
all_cells_list = []
burning_cells = []
# Sets up a matrix of cells with their respective (i, j) values.
for row in range(len(b_grid)):
for column in range(len(b_grid)):
all_cells_list.append([row, column])
# Adds all cells around the burning cells to all_adjacent_cells.
for row in range(len(b_grid)):
for column in range(len(b_grid)):
if b_grid[row][column] is True:
all_adjacent_cells.append([row - 1, column - 1])
all_adjacent_cells.append([row - 1, column])
all_adjacent_cells.append([row - 1, column + 1])
all_adjacent_cells.append([row, column + 1])
all_adjacent_cells.append([row + 1, column + 1])
all_adjacent_cells.append([row + 1, column])
all_adjacent_cells.append([row + 1, column - 1])
all_adjacent_cells.append([row, column - 1])
burning_cells.append([row, column])
# Adds cells to all_adjacent_cells based on the wind.
for burning_cell in burning_cells:
if wind == 'N':
row_adjustment = -2
for column_adjustment in [-1, 0, 1]:
all_adjacent_cells.append([burning_cell[0] + row_adjustment,
burning_cell[1] +
column_adjustment])
elif wind == 'NW':
for row_adjustment in [-1, -2]:
for column_adjustment in [-1, -2]:
if (row_adjustment, column_adjustment) != (-1, -1):
all_adjacent_cells.append([burning_cell[0] +
row_adjustment,
burning_cell[1] +
column_adjustment])
elif wind == 'W':
column_adjustment = -2
for row_adjustment in [-1, 0, 1]:
all_adjacent_cells.append([burning_cell[0] + row_adjustment,
burning_cell[1] +
column_adjustment])
elif wind == 'SW':
for row_adjustment in [-1, -2]:
for column_adjustment in [1, 2]:
if (row_adjustment, column_adjustment) != (-1, 1):
all_adjacent_cells.append([burning_cell[0] +
row_adjustment,
burning_cell[1] +
column_adjustment])
elif wind == 'S':
row_adjustment = 2
for column_adjustment in [-1, 0, 1]:
all_adjacent_cells.append([burning_cell[0] + row_adjustment,
burning_cell[1] +
column_adjustment])
elif wind == 'SE':
for row_adjustment in [1, 2]:
for column_adjustment in [1, 2]:
if (row_adjustment, column_adjustment) != (1, 1):
all_adjacent_cells.append([burning_cell[0] +
row_adjustment,
burning_cell[1] +
column_adjustment])
elif wind == 'E':
column_adjustment = 2
for row_adjustment in [-1, 0, 1]:
all_adjacent_cells.append([burning_cell[0] + row_adjustment,
burning_cell[1] +
column_adjustment])
elif wind == 'NE':
for row_adjustment in [-1, -2]:
for column_adjustment in [1, 2]:
if (row_adjustment, column_adjustment) != (-1, 1):
all_adjacent_cells.append([burning_cell[0] +
row_adjustment,
burning_cell[1] +
column_adjustment])
# Removes cells from adjacent_cells_list that don't fit the size of the
# grid, are already in adjacent_cells_list, or are in burning_cells.
for cell in all_adjacent_cells:
if cell in all_cells_list:
if cell not in burning_cells:
if cell not in adjacent_cells_list:
adjacent_cells_list.append(cell)
return adjacent_cells_list
def get_height_impact_model(h_grid, burning_cells):
'''Creates a matrix of the same size as h_grid with the values of each
cell (i, j) equal to the factor by which ignition_factor is affected by
(e.g. [[0.5], [0.5], [2], [1]]). This is used so that the ignition factor
for each cell can be multiplied by this factor that is influenced by the
height.'''
all_cells_list = []
height_impact_list = []
# Sets up a matrix of cells with their respective (i, j) values, and
# generates a black matrix height_impact_list, which is to be filled in
# later.
for row in range(len(h_grid)):
height_impact_list.append([])
for column in range(len(h_grid)):
all_cells_list.append([row, column])
height_impact_list[row].append([])
# Generates a value for each cell based on the relative value of the height
# of that cell to the height of the burning cells- either 0.5, 1, or 2.
for x in range(len(burning_cells)):
# Generates a list of adjacent cells for each burning cell.
burning_cell_x = burning_cells[x]
row = burning_cell_x[0]
column = burning_cell_x[1]
directly_adjacent_cells = []
directly_adjacent_cells.append([row - 1, column - 1])
directly_adjacent_cells.append([row - 1, column])
directly_adjacent_cells.append([row - 1, column + 1])
directly_adjacent_cells.append([row, column + 1])
directly_adjacent_cells.append([row + 1, column + 1])
directly_adjacent_cells.append([row + 1, column])
directly_adjacent_cells.append([row + 1, column - 1])
directly_adjacent_cells.append([row, column - 1])
# Finds the height of the burning cell currently in the loop and
# compares it to the cells surrounding this cell, and assigns the cell
# an ignition multiplication factor.
burning_cell_x_i = burning_cell_x[0]
burning_cell_x_j = burning_cell_x[1]
burning_cell_x_height = h_grid[burning_cell_x_i][burning_cell_x_j]
# Removes cells from directly_adjacent_cells that don't fit the size of
# the grid.
direct_adjacent_cells = []
for cell in directly_adjacent_cells:
if cell in all_cells_list:
direct_adjacent_cells.append(cell)
for cell in direct_adjacent_cells:
cell_i = cell[0]
cell_j = cell[1]
cell_height = h_grid[cell_i][cell_j]
if cell_height < burning_cell_x_height:
height_impact_list[cell_i][cell_j] = 0.5
elif cell_height == burning_cell_x_height:
height_impact_list[cell_i][cell_j] = 1
elif cell_height > burning_cell_x_height:
height_impact_list[cell_i][cell_j] = 2
return height_impact_list
def check_ignition(b_grid, f_grid, h_grid, i_threshold, w_direction, i, j):
'''Returns True or False if a cell at (i, j) will start burning in the next
timestep based on the factors influencing it's likelihood to burn, as
described to the right.'''
# Defines values to be used in this function based on the inputs.
fuel_load = f_grid[i][j]
wind = w_direction
ignition_threshold = i_threshold
input_cell_is_burning = b_grid[i][j]
burning_cells = []
cells_list = []
for row in range(len(b_grid)):
for column in range(len(b_grid)):
if b_grid[row][column] is True:
burning_cells.append([row, column])
for row in range(len(b_grid)):
for column in range(len(b_grid)):
cells_list.append([row, column])
# Gets the cells adjacent to the burning cells, and the impact that the
# height has on the ignition factor of each cell.
adjacent_cells = get_adjacent_cells(b_grid, wind)
height_model = get_height_impact_model(h_grid, burning_cells)
# Adds all cells around the input cell to directly_adjacent_cells. Note
# that directly_adjacent_cells contains cells outside the matrix, whereas
# direct_cells_list does not.
directly_adjacent_cells = []
direct_cells_list = []
directly_adjacent_cells.append([i - 1, j - 1])
directly_adjacent_cells.append([i - 1, j])
directly_adjacent_cells.append([i - 1, j + 1])
directly_adjacent_cells.append([i, j + 1])
directly_adjacent_cells.append([i + 1, j + 1])
directly_adjacent_cells.append([i + 1, j])
directly_adjacent_cells.append([i + 1, j - 1])
directly_adjacent_cells.append([i, j - 1])
# Removes cells from directly_adjacent_cells that don't fit the size of the
# grid.
for cell in directly_adjacent_cells:
if cell in cells_list:
direct_cells_list.append(cell)
# Calculates the ignition factor.
ignition_factor = 0
for cell in direct_cells_list:
if cell in adjacent_cells:
ignition_factor += 1
ignition_factor = height_model[i][j] * ignition_factor
# Determines what to return based on ignition_factor, fuel_load and
# input_cell_is_burning.
if (input_cell_is_burning is True) and (fuel_load > 0):
return True
elif ignition_factor > ignition_threshold:
return True
return False

92
part3.py Normal file
View file

@ -0,0 +1,92 @@
# Title: Project 2 - Run the model
# Author: Rory Healy
# Date created - 9th May 2019
# Create global variables to be accessed in multiple functions.
times_iterated = 0
current_state = []
previous_state = []
b_grid_initial = []
def create_b_grid(f_grid, burn_seeds):
'''Creates a matrix of size M, b_grid, which indicates whether a cell is
currently burning or not.'''
b_grid = []
for row in range(len(f_grid)):
b_grid.append([])
for column in range(len(f_grid)):
b_grid[row].append([])
for cell in burn_seeds:
for row in range(len(b_grid)):
for column in range(len(b_grid)):
if (row, column) == cell:
b_grid[row].remove([])
b_grid[row].append(True)
else:
b_grid[row].remove([])
b_grid[row].append(False)
return b_grid
def iterate_time(f_grid, h_grid, i_threshold, w_direction, b_grid):
'''Takes the conditions of the current state and iterates the grid so that
exactly 1 unit of time passes. Returns a list of the updated f_grid and
burn_seeds.'''
global current_state
# Creates a new b_grid and f_grid to be returned. Updates b_grid_new and
# f_grid_new to match the changes in the next timestep.
b_grid_new = b_grid
f_grid_new = f_grid
for cell in f_grid:
i = cell[0]
j = cell[1]
if check_ignition(b_grid, f_grid, h_grid, i_threshold, w_direction, i,
j) is True:
f_grid_new[cell] -= 1
for row in b_grid_new:
for column in b_grid_new:
b_grid_new[row][column] = True
current_state = [f_grid_new, b_grid_new]
return current_state
def run_model(f_grid, h_grid, i_threshold, w_direction, burn_seeds):
'''Takes the initial conditions as given above and returns a tuple
containing the final state of the grid once the fire has gone out, and the
total number of cells that were burned.'''
global times_iterated
global current_state
global previous_state
global b_grid_initial
# Calls iterate_time for the first time in order to generate a new value
# for current_state to be compared to the previous state.
if times_iterated == 0:
b_grid = create_b_grid(f_grid, burn_seeds)
b_grid_initial = create_b_grid(f_grid, burn_seeds)
current_state = [f_grid, b_grid]
times_iterated = 1
previous_state = current_state
current_state = iterate_time(f_grid, h_grid, i_threshold, w_direction,
b_grid)
# Either returns the final state and total burned cells, or continues to
# the next timestep and calls run_model again, repeating until the states
# aren't changing anymore.
elif current_state == previous_state:
total_burned_cells = len(burn_seeds)
for row in b_grid_initial:
for cell in b_grid_initial:
if cell == b_grid[row][cell]:
total_burned_cells += 1
return (current_state, total_burned_cells)
else:
times_iterated += 1
previous_state = current_state
current_state = iterate_time(f_grid, h_grid, i_threshold, w_direction,
b_grid)
run_model(f_grid, h_grid, i_threshold, w_direction, burn_seeds)

29
part4.py Normal file
View file

@ -0,0 +1,29 @@
# Title: Project 2 - Test cases
# Author: Rory Healy
# Date created - 9th May 2019
from testcase_tournament import test_fn as test_run_model
# 1) Tests if the model decreases the fuel_load of each cell by 1 for each
# timestep.
test_run_model([[[3, 2, 5], [1, 1, 5], [4, 2, 7]], [[1, 1, 1], [1, 1, 1], [1, 1, 1]], 1, None, [(1, 1)]], [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], 9])
# 2) Tests if the model decreases the fuel_load of each cell when the ignition
# threshold is increased.
test_run_model([[[2, 2, 2], [2, 1, 2], [2, 2, 2]], [[1, 1, 1], [1, 1, 1], [1, 1, 1]], 2, None, [(1, 1)]], [[[2, 2, 2], [2, 0, 2], [2, 2, 2]], 1])
# 3) Tests if the wind affects the number of adjacent cells that would catch
# on fire.
test_run_model([[[2, 2, 2], [2, 1, 2], [2, 2, 2]], [[1, 1, 1], [1, 1, 1], [1, 1, 1]], 1, 'NW', [(2, 2)]], [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], 9])
# 4) and 5) Tests examples given in Q3. These will test how multiple changes to
# the input values affect the resulting output, such as changing f_grid,
# w_direction, and i_threshold.
test_run_model([[[2, 2], [2, 2]], [[1, 1], [1, 1]], 1, 'N', [(0, 0)]], [[[0, 0], [0, 0]], 4])
test_run_model([[[2, 0], [0, 2]], [[1, 1], [1, 1]], 2, 'S', [(0, 0)]], [[[0, 0], [0, 2]], 1])
# 6) Mutations of tests 4) and 5), but changing height as well as burn_seeds.
test_run_model([[[2, 1], [1, 3]], [[1, 2], [1, 2]], 1, 'N', [(1, 0)]], [[[0, 0], [0, 0]], 4])
# 7) Testing a really large matrix size.
test_run_model([[[1, 1, 1, 2, 2, 2, 3], [3, 2, 3, 4, 3, 1, 2], [6, 2, 8, 1, 3, 1, 2], [2, 2, 1, 3, 4, 2, 1], [2, 3, 2, 1, 2, 1, 3], [1, 1, 1, 1, 2, 1, 1], [1, 1, 1, 1, 1, 1, 1]], [[1, 1, 1, 2, 2, 2, 3], [1, 1, 2, 2, 3, 3, 4], [1, 2, 2, 3, 4, 4, 3], [1, 2, 3, 3, 4, 3, 2], [1, 2, 2, 3, 4, 3, 2], [1, 2, 2, 3, 4, 4, 3], [1, 1, 2, 2, 3, 3, 4]], 1, 'N', [(3, 5)]], [[[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], 49])

2
part5.py Normal file
View file

@ -0,0 +1,2 @@
def plan_burn(f_grid, h_grid, i_threshold, town_cell):
pass

474
project02 Normal file
View file

@ -0,0 +1,474 @@
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 ci,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
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 (zero) 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 (zero).
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 (zero). 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 on 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 of eight
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)
- 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 'bf0.dat' 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 c1,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 (ie, the landscape slopes up
toward the East)
- the ignition threshold is 1
- a wind is blowing from the North
- the top left cell c0,0 is burning at the start of the simulation
You may assume that the input files are well-formed, but your function should
ensure that:
- 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
- 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('bf0.dat')
{'f_grid': [[2, 2], [0, 2]],
'h_grid': [[1, 2], [1, 2]],
'i_threshold': 1,
'w_direction': 'N',
'burn_seeds': [(0, 0)]}
>>> parse_scenario('bf.dat')
{'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 on the 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, 3, ...
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 on 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 ci,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 (zero) in order to catch fire.
We will start by considering the simplest case: a flat landscape with no wind in
which cell ci,j is not currently burning. In this case, each cell adjacent to
ci,j that is burning contributes 1 point to ci,j's ignition factor. Thus, if the
ignition threshold is 1, ci,j will start burning if it is adjacent to at least
one burning cell. If the ignition threshold is 2, ci,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 c0,0 and c0,1 are
burning. During this timestep, the fuel load of each of these two cells will
decrease by one (causing the fire in c0,0 to stop burning in the next time step,
t = 1). If we then consider at the surrounding cells, c1,0 has a fuel load of 0
(zero), so it cannot catch fire. Cells c0,2, c1,1 and c1,2 each have a fuel load
greater than 0 (zero) and are adjacent to one of the currently burning cells,
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
simple_bushfire_model.png, 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 c0,2 and cells c1,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 on the previous section. The effect of landscape height is described on
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 ci,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 ci,j's ignition factor. Conversely,
bushfires tend to spread more slowly downhill, therefore an adjacent burning
cell with height greater than ci,j's height will contribute only half as much
(i.e., 0.5 points) to ci,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 c0,0 is
burning. The cell to the South of it (c1,0) has a fuel load of 0 (zero) and
hence cannot catch fire. On a flat landscape, the other two adjacent cells (c0,1
and c1,1 would not catch fire either, as their ignition factor would only be 1
(from the single burning cell c0,0), below the ignition threshold of 2. However,
in this landscape, cells c0,1 and c1,1 are higher than cell c0,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 c0,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, c0,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 ci,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 ci,j and the cells immediately to the left and right
of this cell are considered adjacent to ci,j (ie, cells ci2,j1, ci2,j and
ci2,j+1). If any of these additional cells are burning, they will also
contribute when calculating ci,j's ignition factor.
If a wind is blowing from the Southwest, the cell two cells below and to the
left of ci,j and the cells immediately above and to the right of this cell are
considered adjacent to ci,j. That is, cells ci+1,j2, ci+2,j2 and ci+2,j1. Of
course, these additional cells must be within the bounds of the landscape in
order to have any effect, as in the simple case on the previous section.
The sequence shown in wind_bushfire_model.png is identical to the simple example
shown on 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
c0,0 is considered adjacent to c1,2, which therefore has an ignition factor of
2 (as c0,1 is also adjacent and burning) and hence will start burning at t = 1.
In addition, c0,0 and c0,1 are also both considered adjacent to c2,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 ci,j and each of its adjacent cells on a pairwise basis, disregarding
the heights of any other surrounding cells. For example, if c0,0 was higher than
c2,2 and c0,1 was lower than c2,2 then they would contribute 0.5 points and 2
points respectively to c2,2's ignition factor, irrespective of the heights of the
intervening cells (c1,1, etc).
--------------------------------------------------------------------------------
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 questions.
You have been provided with a reference version of the function check_ignition
as described in Part 2.
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; ie, 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(input_args, expected_return_value), where the first 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).
That is, you specify both the arguments and return value for run_model as
arguments to test_run_model.
For example, using the first two examples from Part 2:
>>> from testcase_tournament import test_fn as test_run_model
>>> test_run_model([[[2, 2], [2, 2]], [[1, 1], [1, 1]], 1, 'N', [(0, 0)]],
[[[0, 0], [0, 0]], 4])
>>> test_run_model([[[2, 0], [0, 2]], [[1, 1], [1, 1]], 2, 'S', [(0, 0)]],
[[[0, 0], [0, 2]], 1])
--------------------------------------------------------------------------------
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 (M22)×9 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 (c1,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 (c0,1), all
subsequent bushfires will result in the town catching fire. For the other two
prescribed burn cells (c0,0 and c1,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
project02-rubric.pdf Executable file

Binary file not shown.

View file

@ -0,0 +1,409 @@
# ------------------------------------------------------------------------------
# Part 1 - Parse a bushfire scenario file
# Sample solution 1
def parse_scenario_solution_1(filename):
# read file
with open(filename) as f:
# get size
grid_size = int(f.readline())
# get fuel load grid
f_grid = []
for i in range(grid_size):
row = f.readline()
f_grid.append([int(x) for x in row.strip().split(',')])
# get height grid
h_grid = []
for i in range(grid_size):
row = f.readline()
h_grid.append([int(x) for x in row.strip().split(',')])
# get ignition threshold
i_threshold = int(f.readline())
# get wind direction
w_direction = f.readline().strip()
if w_direction == 'None':
w_direction = None
# get initial burning cells
burn_seeds = []
for row in f.readlines():
x, y = [int(x) for x in row.strip().split(',')]
burn_seeds.append((x, y))
# validate values
if i_threshold > 8 or i_threshold < 1:
return None
if w_direction not in ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'None']:
return None
for i, j in burn_seeds:
if i > grid_size-1 or j > grid_size-1:
return None
if f_grid[i][j] < 1:
return None
return {'f_grid': f_grid, 'h_grid': h_grid,
'i_threshold': i_threshold, 'w_direction': w_direction, 'burn_seeds':
burn_seeds}
# Sample solution 2
WIND_DIRECTIONS = {'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', None}
def is_valid(data):
size = len(data['f_grid'])
if data['i_threshold'] < 1 or data['i_threshold'] > 8:
return False
if data['w_direction'] not in WIND_DIRECTIONS:
return False
for r, c in data['burn_seeds']:
if r < 0 or r >= size or c < 0 or c >= size:
return False
elif data['f_grid'][r][c] < 1:
return False
return True
def parse_scenario_solution_2(filename):
# Read the whole file.
with open(filename) as f:
lines = f.readlines()[::-1]
# Extract the size of the grid.
size = int(lines.pop())
# Extract the initial fuel values for each cell in the grid.
fuels = [list(map(int, lines.pop().split(','))) for r in range(size)]
# Extract the height values for each cell in the grid.
heights = [list(map(int, lines.pop().split(','))) for r in range(size)]
# Extract the ignition threshold.
ignition_threshold = int(lines.pop())
# Extract the initial wind direction.
wind_direction = lines.pop().strip()
if wind_direction == 'None':
wind_direction = None
# Extract the grid locations for the cells that are initially burning.
burning_cells = [tuple(map(int, line.split(','))) for line in lines[::-1]]
# Validate the data and return appropriately.
data = {
'f_grid': fuels,
'h_grid': heights,
'i_threshold': ignition_threshold,
'w_direction': wind_direction,
'burn_seeds': burning_cells,
}
return data if is_valid(data) else None
# ------------------------------------------------------------------------------
# Part 2 - Determine if a cell starts burning
# Sample solution 1
# ignition factors
UPHILL = 2
DOWNHILL = 0.5
def check_ignition_solution_1(b_grid, f_grid, h_grid, i_threshold, w_direction, i, j):
# False if no fuel at (i, j) or (i, j) already burning
if b_grid[i][j] or not f_grid[i][j]:
return False
# neighbouring cells
n_list = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
# supplement neighbour list based on wind direction
if w_direction == 'N':
n_list += [(-2, -1), (-2, 0), (-2, 1)]
elif w_direction == 'NE':
n_list += [(-2, 1), (-2, 2), (-1, 2)]
elif w_direction == 'E':
n_list += [(-1, 2), (0, 2), (1, 2)]
elif w_direction == 'SE':
n_list += [(1, 2), (2, 2), (2, 1)]
elif w_direction == 'S':
n_list += [(2, 1), (2, 0), (2, -1)]
elif w_direction == 'SW':
n_list += [(2, -1), (2, -2), (1, -2)]
elif w_direction == 'W':
n_list += [(-1, -2), (0, -2), (1, -2)]
elif w_direction == 'NW':
n_list += [(-1, -2), (-2, -2), (-2, -1)]
else:
pass # no (valid) wind direction
# get size
grid_size = len(b_grid)
# calculate ignition factor
i_factor = 0
for (d_i, d_j) in n_list:
# check neighbour is on the grid
if not (0 <= i + d_i < grid_size and 0 <= j + d_j < grid_size):
continue
# fire spreading uphill
if h_grid[i + d_i][j + d_j] < h_grid[i][j]:
i_factor += UPHILL * b_grid[i + d_i][j + d_j]
# fire spreading downhill
elif h_grid[i + d_i][j + d_j] > h_grid[i][j]:
i_factor += DOWNHILL * b_grid[i + d_i][j + d_j]
# fire spreading on level
else:
i_factor += b_grid[i + d_i][j + d_j]
return i_factor >= i_threshold
# Sample solution 2
WIND_DIRECTION_DELTAS = {
'N': [(-2, -1), (-2, 0), (-2, 1)],
'NE': [(-2, 1), (-2, 2), (-1, 2)],
'E': [(-1, 2), (0, 2), (1, 2)],
'SE': [(1, 2), (2, 2), (2, 1)],
'S': [(2, 1), (2, 0), (2, -1)],
'SW': [(1, -2), (2, -2), (2, -1)],
'W': [(-1, -2), (0, -2), (1, -2)],
'NW': [(-2, -1), (-2, -2), (-1, -2)],
None: [],
}
MOVE_DELTAS = [
(-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (1, 1),
]
# ignition factors
UPHILL = 2.0
LEVEL = 1.0
DOWNHILL = 0.5
def check_ignition_solution_2(b_grid, f_grid, h_grid, i_threshold, w_direction, i, j):
# If the cell is currently burning, bail.
if b_grid[i][j]:
return False
# If the cell has no fuel, bail.
if f_grid[i][j] == 0:
return False
# Work out the cells we need to check relative to the given cell.
deltas = list(MOVE_DELTAS)
if w_direction != '0':
deltas += WIND_DIRECTION_DELTAS[w_direction]
# Compute the ignition factor.
i_factor = 0.0
size = len(b_grid)
for di, dj in deltas:
# Compute the i, j value of the new cell.
ni, nj = i + di, j + dj
# Ensure that new cell is on the grid and that it's currently burning.
if ni < 0 or ni >= size or nj < 0 or nj >= size:
continue
elif not b_grid[ni][nj]:
continue
# Account for the height differential between cells.
dh = h_grid[ni][nj] - h_grid[i][j]
if dh == 0:
i_factor += LEVEL
elif dh < 0:
i_factor += UPHILL
else:
i_factor += DOWNHILL
return i_factor >= i_threshold
# ------------------------------------------------------------------------------
# Part 3 - Run the model
# Sample solution 1
def update_state(b_grid, f_grid, h_grid, i_threshold, w_direction):
# create matrices to store next state
b_grid_next = [[x for x in y] for y in b_grid]
f_grid_next = [[x for x in y] for y in f_grid]
ignitions = 0
grid_size = len(b_grid)
# update each cell in turn
for i in range(grid_size):
for j in range(grid_size):
# handle fuel depletion
if b_grid[i][j]:
f_grid_next[i][j] -= 1
# extinguish fire at (i, j) if fuel depleted
if f_grid_next[i][j] == 0:
b_grid_next[i][j] = False
# check for new ignitions
else:
new_ignition = check_ignition(b_grid, f_grid, h_grid,
i_threshold, w_direction, i, j)
if new_ignition:
ignitions += 1
b_grid_next[i][j] = True
return b_grid_next, f_grid_next, ignitions
def burning(b_grid):
# check if any cells are burning
r = [any(x) for x in b_grid]
return any(r)
def run_model(f_grid, h_grid, i_threshold, w_direction, seed_cells):
grid_size = len(f_grid)
# initialise burn grid
b_grid = [[False for _ in range(grid_size)] for _ in range(grid_size)]
burnt_cells = 0
for i, j in set(seed_cells):
if f_grid[i][j] > 0:
b_grid[i][j] = 1
burnt_cells += 1
# repeat updates while there is still fire present
while burning(b_grid):
b_grid, f_grid, ignitions = update_state(b_grid, f_grid, h_grid, i_threshold, w_direction)
burnt_cells += ignitions
return f_grid, burnt_cells
# Sample solution 2
import copy
import itertools
def run_model(f_grid, h_grid, i_threshold, w_direction, burn_seeds):
size = len(f_grid)
# Keep a set of all (i, j) pairs that have been burnt by fire.
burnt = set()
# Compute the initial burn grid.
b_grid = [[False] * size for _ in range(size)]
for i, j in burn_seeds:
b_grid[i][j] = True
burnt.add((i, j))
# Run the simulation while at least one cell is burning.
while any(itertools.chain.from_iterable(b_grid)):
new_b_grid = copy.deepcopy(b_grid)
new_f_grid = copy.deepcopy(f_grid)
# Work out what new cell should ignite.
for i in range(size):
for j in range(size):
if check_ignition(b_grid, f_grid, h_grid,
i_threshold, w_direction, i, j):
new_b_grid[i][j] = True
burnt.add((i, j))
# Decrease fuel for all currently burning cells.
for i in range(size):
for j in range(size):
if b_grid[i][j]:
new_f_grid[i][j] -= 1
if new_f_grid[i][j] == 0:
new_b_grid[i][j] = False
# Update our burn state for the next iteration.
b_grid = new_b_grid
f_grid = new_f_grid
return (f_grid, len(burnt))
# ------------------------------------------------------------------------------
# Part 4 - Test cases
# No solution provided.
# ------------------------------------------------------------------------------
# Part 5 - Bonus
from collections import defaultdict
# valid wind directions
w_dirs = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', None]
def test_burn(f_grid, h_grid, i_threshold, town_cell):
'''
Run all possible prescribed burns on a landscape containing a town,
returning a dictionary of valid prescribed burn cells, together with the
fuel load following that burn.
'''
grid_size = len(f_grid)
town_i, town_j = town_cell
# maps valid prescribed burn cells to the subsequent fuel load matrix.
valid_burns = {}
# test prescribed burn from each valid starting cell
for i in range(grid_size):
for j in range(grid_size):
cur_seed = (i, j)
# skip town cell, and cells with zero fuel load
if cur_seed == town_cell or f_grid[i][j] == 0:
continue
# test prescribed burn
new_f_grid, burnt = run_model(f_grid, h_grid,
i_threshold * 2, None, [cur_seed])
# if town did not catch fire, add seed to valid burn cell dictionary
if new_f_grid[town_i][town_j] == f_grid[town_i][town_j]:
valid_burns[(cur_seed)] = new_f_grid
return valid_burns
def test_fire(f_grid, h_grid, i_threshold, town_cell):
'''
evaluate all possible bushfires (each starting cell, except town, and
each wind direction), returning the proportion of scenarios in which
the town was burnt
'''
grid_size = len(f_grid)
town_i, town_j = town_cell
# store True if town burnt, otherwise False
town_burnt = []
# test each starting cell
for i in range(grid_size):
for j in range(grid_size):
# skip town cell, and cells with zero fuel load
if (i, j) == town_cell or f_grid[i][j] == 0:
continue
# test each wind direction
for cur_w_dir in w_dirs:
new_f_grid, burnt = run_model(f_grid, h_grid,
i_threshold, cur_w_dir, [(i, j)])
# keep track of whether town was burnt
town_burnt.append(new_f_grid[town_i][town_j]
< f_grid[town_i][town_j])
if town_burnt:
return sum(town_burnt) / len(town_burnt)
else:
return 0
def plan_burn(f_grid, h_grid, i_threshold, town_cell):
'''
determine the optimal cells in which to conduct a prescribed burn in order
to reduce the probability of a future bushfire burning the town cell.
'''
# determine valid burn cells
valid_burns = test_burn(f_grid, h_grid, i_threshold, town_cell)
# build dictionary mapping burn scores to burn seeds
burn_scores = defaultdict(list)
# calculate burn score for each valid burn cell
for cur_seed, cur_f_grid in valid_burns.items():
cur_burnt = test_fire(cur_f_grid, h_grid, i_threshold, town_cell)
burn_scores[cur_burnt].append(cur_seed)
# sort burn scores, sort seeds for each burn score,
# and return list of optimal burn seeds
if burn_scores:
return [sorted(burn_scores[k]) for k in sorted(burn_scores)][0]
else:
return []

BIN
simple_bushfire_model.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
simple_bushfire_model_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
wind_bushfire_model.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
wind_example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB