comp10001-project02/project02-sample-solutions.py

409 lines
13 KiB
Python

# ------------------------------------------------------------------------------
# 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 []