410 lines
13 KiB
Python
410 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 []
|