Research Software Design by Example
dataclasses
module to create a Params
class in params_single.py
@dataclass
class ParamsSingle:
"""A single set of invasion percolation parameters."""
kind: str
width: int
height: int
depth: int
seed: int = None
def get_params(filename, cls):
"""Get parameters."""
with open(filename, 'r') as reader:
d = json.load(reader)
return cls(**d)
invperc_single.py
to use these parametersdef main():
"""Main driver."""
params = get_params(sys.argv[1], ParamsSingle)
initialize_random(params)
grid = initialize_grid(params)
num_filled = grid.fill()
if len(sys.argv) > 2:
print_grid(params, grid, num_filled, details='full')
@dataclass
class ParamsSweep:
"""A range of invasion percolation parameters."""
kind: list[str]
size: list[int]
depth: list[int]
runs: int
seed: int = None
main
to try each combination of parameter valuesdef main():
"""Main driver."""
params = get_params(sys.argv[1], ParamsSweep)
initialize_random(params)
results = []
for p in generate_sweep(params):
print(p)
grid = initialize_grid(p)
t_start = time.time()
num_filled = grid.fill()
t_elapsed = time.time() - t_start
results.append(record_result(p, num_filled, t_elapsed))
save_results(params, results)
def generate_sweep(params):
"""Generate next single parameter object."""
for kind in params.kind:
for size in params.size:
for depth in params.depth:
for run in range(params.runs):
yield ParamsSingle(kind, size, size, depth)
cProfile
modulesys.argv = ["invperc_single.py", "profile_list.json"]
cProfile.run("main()", sort="tottime")
13885219 function calls in 3.169 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1794596 1.541 0.000 2.443 0.000 grid_generic.py:47(adjacent)
8601051 0.872 0.000 0.872 0.000 grid_list.py:20(__getitem__)
356 0.551 0.002 3.161 0.009 invperc_util.py:12(choose_cell)
1708120 0.101 0.000 0.101 0.000 grid_generic.py:39(height)
1731763 0.096 0.000 0.096 0.000 grid_generic.py:35(width)
…more lines…
candidates
candidates
GridLazy
constructordef __init__(self, width, height, depth):
"""Construct and fill."""
super().__init__(width, height, depth)
self._candidates = {}
def fill(self):
"""Fill grid one cell at a time."""
x, y = (self.width() // 2, self.height() // 2)
self[x, y] = 0
num_filled = 1
self.add_candidates(x, y)
while True:
x, y = self.choose_cell()
self[x, y] = 0
num_filled += 1
if self.on_border(x, y):
break
return num_filled
def add_candidates(self, x, y):
"""Add candidates around (x, y)."""
for ix in (x - 1, x + 1):
self.add_one_candidate(ix, y)
for iy in (y - 1, y + 1):
self.add_one_candidate(x, iy)
def add_one_candidate(self, x, y):
"""Add (x, y) if suitable."""
if x < 0 or x >= self.width() or y < 0 or (y >= self.height()):
return
if self[x, y] == 0:
return
value = self[x, y]
if value not in self._candidates:
self._candidates[value] = set()
self._candidates[value].add((x, y))
def choose_cell(self):
"""Choose the next cell to fill."""
min_key = min(self._candidates.keys())
available = list(sorted(self._candidates[min_key]))
i = random.randrange(len(available))
choice = available[i]
del available[i]
if not available:
del self._candidates[min_key]
else:
self._candidates[min_key] = set(available)
self.add_candidates(*choice)
return choice
FIXME: add exercises for performance profiling
Modify the list and array implementation to collect candidate cells of equal lowest value and select one of those.
Does it make sense to pre-populate candidates
by adding all cells in the grid
at the start of the program?
Why or why not?
FIXME: test lazy approach with randomnmess