Initial commit. Note - this is the original assignment submission. No modifications have been made.
This commit is contained in:
commit
f4235d6e3c
16 changed files with 1324 additions and 0 deletions
10
bf.dat
Normal file
10
bf.dat
Normal 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
8
bf0.dat
Normal file
|
@ -0,0 +1,8 @@
|
|||
2
|
||||
2,2
|
||||
0,2
|
||||
1,2
|
||||
1,2
|
||||
1
|
||||
N
|
||||
0,0
|
BIN
bushfire_model.png
Normal file
BIN
bushfire_model.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
BIN
height_bushfire_model.png
Normal file
BIN
height_bushfire_model.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
75
part1.py
Normal file
75
part1.py
Normal 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
225
part2.py
Normal 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
92
part3.py
Normal 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
29
part4.py
Normal 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
2
part5.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
def plan_burn(f_grid, h_grid, i_threshold, town_cell):
|
||||
pass
|
474
project02
Normal file
474
project02
Normal 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 ci−2,j−1, ci−2,j and
|
||||
ci−2,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,j−2, ci+2,j−2 and ci+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 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 (M2−2)×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
BIN
project02-rubric.pdf
Executable file
Binary file not shown.
409
project02-sample-solutions.py
Normal file
409
project02-sample-solutions.py
Normal 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
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
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
BIN
wind_bushfire_model.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
BIN
wind_example.png
Normal file
BIN
wind_example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 143 KiB |
Loading…
Reference in a new issue