Turns out on part 2 you can remove on access rather than after a full sweep of the grid, which cuts down the number of iterations you need to do about 1/2 sometimes 1/3 (depending on input).
import itertools as it
from pathlib import Path
import numpy as np
cwd = Path(__file__).parent.resolve()
def parse_input(file_path):
with file_path.open("r") as fp:
grid = np.array(list(map(list, list(map(str.strip, fp.readlines())))),
dtype=str)
return grid
def solve_problem(file_name, single_attempt=True):
grid = parse_input(Path(cwd, file_name))
nr, nc = grid.shape
nacc_total = 0
stop = False
while not stop:
nacc = 0
for i,j in it.product(range(nr), range(nc)):
if grid[i,j] != '@':
continue
if np.count_nonzero(grid[max(i-1, 0):min(i+2, nr),
max(j-1, 0):min(j+2, nc)] == '@')<5:
nacc += 1
if not single_attempt:
grid[i,j] = '.'
nacc_total += nacc
if nacc==0 or single_attempt:
stop = True
return nacc_total