Scale a Grid Fire-Spread Simulation Past What Fits in Memory
Applied IntuitionSoftware Engineer · Technical ScreenLast updated
Scale a working small-grid cellular-automaton fire-spread simulation to a dense multi-million-cell board that no longer fits in memory, by computing each generation with a three-row sliding window read from one file and streamed into another. Tests memory estimation and disk-backed algorithm redesign without changing the core update rule.
A terrain simulation models wildfire spread on a grid of cells, each either burning (1) or clear (0). Every generation updates all cells simultaneously from the previous one, using a fixed local rule based on each cell's eight neighbors (horizontal, vertical, and diagonal).
You're given working code for a small grid, built around an in-memory 2D list:
import itertools
import random
_NEIGHBOR_OFFSETS = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
class FireGrid:
def __init__(self, rows, cols, p):
self.rows, self.cols = rows, cols
self.grid = [random.choices([1, 0], weights=[p, 1 - p], k=cols) for _ in range(rows)]
def step(self):
updated = [[0] * self.cols for _ in itertools.repeat(None, self.rows)]
for row, col in itertools.product(range(self.rows), range(self.cols)):
self._ignite_if_needed(self.grid, updated, row, col)
self.grid = updated
def _ignite_if_needed(self, old, new_grid, row, col):
in_bounds = lambda rr, cc: 0 <= rr < len(old) and 0 <= cc < len(old[rr])
lit = sum(old[row + dr][col + dc] for dr, dc in _NEIGHBOR_OFFSETS if in_bounds(row + dr, col + dc))
already_burning = old[row][col] == 1
new_grid[row][col] = 1 if already_burning or lit >= 3 else 0
Scale this to a grid of 2,000,000 rows by 2,000,000 columns. The ignition probability p could be set close to 1, which means you cannot assume the grid is sparse, so tracking only the handful of burning coordinates is off the table as a shortcut. Leave the logic inside _ignite_if_needed untouched; what needs to change is purely where the grid lives and how one generation gets produced once it no longer fits in RAM.
What to work out
- Estimate the memory a 2,000,000 × 2,000,000 dense grid would need under a few different cell encodings (a Python
intper cell, a byte per cell, a packed bit per cell), and show why the in-memory design above can't survive this scale under any of them without other changes. - Propose storing each row as a line in a file, so the grid lives on disk instead of in memory.
- Design a
stepthat reads generation N from one file and writes generation N+1 to another file, using only a small, fixed amount of memory regardless of grid size — specifically, a sliding window of at most three rows at a time, since that's all_ignite_if_neededever looks at for any one output row.
Constraints
- Treat reading and writing one line at a time from a file as the available primitive; don't assume the whole file can be loaded into memory at once.
- The simulation must still produce the exact same next generation as the original in-memory version would, for the same input grid.
- State what happens to memory usage as the number of rows grows, holding row width fixed.
Hints
Hint 1
Work out the byte count under each encoding you're considering before writing any code — at 4 * 10^12 cells, even one byte per cell is firmly in the terabyte range, and that number alone rules out holding the grid in memory.
Hint 2
_ignite_if_needed for row r only ever reads rows r-1, r, and r+1 — nothing else about the grid matters to compute output row r, which is exactly the fact that makes a bounded sliding window possible.
Hint 3
As you read row r+1 from the source file to compute output row r, you'll need it again to compute output row r+1 — keep it in memory and reuse it instead of re-reading it from disk.
Hint 4
Handle the grid's top and bottom edges as the special case where the 'previous' or 'next' row in the window simply doesn't exist, exactly like the original code's boundary check.
Follow-up questions
- How would you avoid re-reading the entire source file from the OS page cache on every generation if you need to run many generations in sequence?
- How would you parallelize this across multiple processes, given that computing different horizontal bands of one generation is largely independent?
- What changes if cells need more than 1 bit of state, for example tracking how many consecutive generations a cell has been burning?
- How would you checkpoint and resume a run that gets killed partway through writing a generation, without corrupting the output file?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.