commit f4235d6e3c0c2698212854680e459bb033356aba Author: Rory Healy Date: Sat Jun 8 21:17:07 2024 +1000 Initial commit. Note - this is the original assignment submission. No modifications have been made. diff --git a/bf.dat b/bf.dat new file mode 100644 index 0000000..8b1dfab --- /dev/null +++ b/bf.dat @@ -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 diff --git a/bf0.dat b/bf0.dat new file mode 100644 index 0000000..f86d981 --- /dev/null +++ b/bf0.dat @@ -0,0 +1,8 @@ +2 +2,2 +0,2 +1,2 +1,2 +1 +N +0,0 diff --git a/bushfire_model.png b/bushfire_model.png new file mode 100644 index 0000000..1448153 Binary files /dev/null and b/bushfire_model.png differ diff --git a/height_bushfire_model.png b/height_bushfire_model.png new file mode 100644 index 0000000..b28b40e Binary files /dev/null and b/height_bushfire_model.png differ diff --git a/part1.py b/part1.py new file mode 100644 index 0000000..c1aec36 --- /dev/null +++ b/part1.py @@ -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 diff --git a/part2.py b/part2.py new file mode 100644 index 0000000..1f89633 --- /dev/null +++ b/part2.py @@ -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 diff --git a/part3.py b/part3.py new file mode 100644 index 0000000..069fd81 --- /dev/null +++ b/part3.py @@ -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) diff --git a/part4.py b/part4.py new file mode 100644 index 0000000..d9afdd5 --- /dev/null +++ b/part4.py @@ -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]) diff --git a/part5.py b/part5.py new file mode 100644 index 0000000..6c7a4c0 --- /dev/null +++ b/part5.py @@ -0,0 +1,2 @@ +def plan_burn(f_grid, h_grid, i_threshold, town_cell): + pass diff --git a/project02 b/project02 new file mode 100644 index 0000000..c5f8ecf --- /dev/null +++ b/project02 @@ -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)] diff --git a/project02-rubric.pdf b/project02-rubric.pdf new file mode 100755 index 0000000..1fc913e Binary files /dev/null and b/project02-rubric.pdf differ diff --git a/project02-sample-solutions.py b/project02-sample-solutions.py new file mode 100644 index 0000000..d3f3e3c --- /dev/null +++ b/project02-sample-solutions.py @@ -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 [] diff --git a/simple_bushfire_model.png b/simple_bushfire_model.png new file mode 100644 index 0000000..3e9e9fe Binary files /dev/null and b/simple_bushfire_model.png differ diff --git a/simple_bushfire_model_2.png b/simple_bushfire_model_2.png new file mode 100644 index 0000000..4174691 Binary files /dev/null and b/simple_bushfire_model_2.png differ diff --git a/wind_bushfire_model.png b/wind_bushfire_model.png new file mode 100644 index 0000000..b71e4e6 Binary files /dev/null and b/wind_bushfire_model.png differ diff --git a/wind_example.png b/wind_example.png new file mode 100644 index 0000000..f8c6f8b Binary files /dev/null and b/wind_example.png differ