Skip to content

Commit 2ff2164

Browse files
committed
adding implementation of 1D recursive memoization
1 parent ca5d3a8 commit 2ff2164

File tree

2 files changed

+247
-11
lines changed

2 files changed

+247
-11
lines changed

cellpylib/ca_functions.py

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,16 @@ def evolve(cellular_automaton, timesteps, apply_rule, r=1, memoize=False):
8484
8585
:param r: the neighbourhood radius; the neighbourhood size will be 2r + 1 (default is 1)
8686
87-
:param memoize: if True, then the result of applying the rule on a given neighbourhood will be cached, and used on
88-
subsequent invocations of the rule; this can result in a significant improvement to execution speed
89-
if the rule is expensive to invoke; NOTE: this should only be set to True for rules which do not
90-
store any state upon invocation, and for rules which do not depend in the cell index or timestep
91-
number (default is False)
87+
:param memoize: allowed values are True, False, and "recursive"; if True, then the result of applying the rule on a
88+
given neighbourhood will be cached, and used on subsequent invocations of the rule; if "recursive",
89+
then a recursive memoized algorithm will be used, in which recursively wider neighbourhoods are
90+
cached, along with the result of applying the rule on the cells in the widened neighbourhood; the
91+
True and "recursive" options can result in a significant improvement to execution speed if the rule
92+
is expensive to invoke; the "recursive" option works best when there are strongly repetitive
93+
patterns in the CA, and when the state consists of 2^k cells; if False, then no caching will be
94+
used; NOTE: this should only be set to True or "recursive" for rules which do not store any state
95+
upon invocation, and for rules which do not depend in the cell index or timestep number (default is
96+
False)
9297
9398
:return: a matrix, containing the results of the evolution, where the number of rows equal the number of time steps
9499
specified
@@ -119,7 +124,7 @@ def _evolve_fixed(cellular_automaton, timesteps, apply_rule, r, memoize):
119124
120125
:param r: the neighbourhood radius; the neighbourhood size will be 2r + 1
121126
122-
:param memoize: whether to use memoization
127+
:param memoize: the memoization flag; one of True, False, or "recursive"
123128
124129
:return: a matrix, containing the results of the evolution, where the number of rows equal the number of time steps
125130
specified
@@ -128,17 +133,24 @@ def _evolve_fixed(cellular_automaton, timesteps, apply_rule, r, memoize):
128133
_, cols = cellular_automaton.shape
129134
array = np.zeros((timesteps, cols), dtype=cellular_automaton.dtype)
130135
array[0] = initial_conditions
136+
cell_indices = list(range(len(initial_conditions)))
131137

132138
memo_table = {}
133139

134140
for t in range(1, timesteps):
135141
cells = array[t - 1]
136142
strides = _index_strides(np.arange(len(cells)), 2 * r + 1)
137143
neighbourhoods = cells[strides]
138-
if memoize:
144+
if memoize is "recursive":
145+
next_state = np.zeros(len(cells), dtype=cellular_automaton.dtype)
146+
_step(cell_indices, cells, next_state, memo_table, apply_rule, r, t)
147+
array[t] = next_state
148+
elif memoize is True:
139149
array[t] = np.array([_get_memoized(n, c, t, apply_rule, memo_table) for c, n in enumerate(neighbourhoods)])
140-
else:
150+
elif memoize is False:
141151
array[t] = np.array([apply_rule(n, c, t) for c, n in enumerate(neighbourhoods)])
152+
else:
153+
raise Exception("unsupported memoization option: %s" % memoize)
142154

143155
return np.concatenate((cellular_automaton, array[1:]), axis=0)
144156

@@ -164,14 +176,15 @@ def _evolve_dynamic(cellular_automaton, timesteps, apply_rule, r, memoize):
164176
165177
:param r: the neighbourhood radius; the neighbourhood size will be 2r + 1
166178
167-
:param memoize: whether to use memoization
179+
:param memoize: the memoization flag; one of True, False, or "recursive"
168180
169181
:return: a matrix, containing the results of the evolution, where the number of rows equal the number of time steps
170182
specified
171183
"""
172184
initial_conditions = cellular_automaton[-1]
173185
_, cols = cellular_automaton.shape
174186
array = [initial_conditions]
187+
cell_indices = list(range(len(initial_conditions)))
175188

176189
memo_table = {}
177190

@@ -180,16 +193,89 @@ def _evolve_dynamic(cellular_automaton, timesteps, apply_rule, r, memoize):
180193
cells = array[-1]
181194
strides = _index_strides(np.arange(len(cells)), 2 * r + 1)
182195
neighbourhoods = cells[strides]
183-
if memoize:
196+
if memoize is "recursive":
197+
result = np.zeros(len(cells), dtype=cellular_automaton.dtype)
198+
_step(cell_indices, cells, result, memo_table, apply_rule, r, t)
199+
elif memoize is True:
184200
result = [_get_memoized(n, c, t, apply_rule, memo_table) for c, n in enumerate(neighbourhoods)]
185-
else:
201+
elif memoize is False:
186202
result = [apply_rule(n, c, t) for c, n in enumerate(neighbourhoods)]
203+
else:
204+
raise Exception("unsupported memoization option: %s" % memoize)
187205
array.append(np.array(result, dtype=cellular_automaton.dtype))
188206
t += 1
189207

190208
return np.concatenate((cellular_automaton, array[1:]), axis=0)
191209

192210

211+
def _step(indices, curr_state, next_state, cache, apply_rule, r, t):
212+
"""
213+
Perform an update on the given next state using the current state and memoization cache, based on
214+
an even split of the cell indices.
215+
216+
:param indices: a list of the cell indices of the cells to update
217+
218+
:param curr_state: the current state (i.e. the state after the previous timestep)
219+
220+
:param next_state: the next state (i.e. the result after the current timestep)
221+
222+
:param cache: a dictionary that maps state neighbourhoods to their activities
223+
224+
:param apply_rule: the rule to apply during each cell update
225+
226+
:param r: the neighbourhood radius
227+
228+
:param t: the current timestep
229+
"""
230+
mid = len(indices) // 2
231+
left_indices = indices[:mid]
232+
right_indices = indices[mid:]
233+
if len(left_indices) > 0:
234+
_update_state(left_indices, curr_state, next_state, cache, apply_rule, r, t)
235+
if len(right_indices) > 0:
236+
_update_state(right_indices, curr_state, next_state, cache, apply_rule, r, t)
237+
238+
239+
def _update_state(indices, curr_state, next_state, cache, apply_rule, r, t):
240+
"""
241+
Perform an update on the given next state using the current state and memoization cache.
242+
243+
:param indices: a list of the cell indices of the cells to update
244+
245+
:param curr_state: the current state (i.e. the state after the previous timestep)
246+
247+
:param next_state: the next state (i.e. the result after the current timestep)
248+
249+
:param cache: a dictionary that maps state neighbourhoods to their activities
250+
251+
:param apply_rule: the rule to apply during each cell update
252+
253+
:param r: the neighbourhood radius
254+
255+
:param t: the current timestep
256+
"""
257+
# get the state string for the state given by the indices
258+
start = indices[0]
259+
end = indices[-1]
260+
neighbourhood_indices = range(start - r, end + 1 + r)
261+
neighbourhood = curr_state.take(neighbourhood_indices, mode='wrap')
262+
state_string = neighbourhood.tobytes()
263+
264+
if state_string in cache:
265+
# update next_state with next vals from cache
266+
next_state[indices] = cache[state_string]
267+
else:
268+
if len(indices) > 1:
269+
_step(indices, curr_state, next_state, cache, apply_rule, r, t)
270+
else:
271+
# invoke rule and update next_state for cell
272+
val = apply_rule(neighbourhood, start, t)
273+
next_state[start] = val
274+
# get the result from the next_state for the left_indices and place in cache
275+
vals_to_cache = next_state[indices]
276+
cache[state_string] = vals_to_cache
277+
278+
193279
def _get_memoized(n, c, t, apply_rule, memoization_table):
194280
"""
195281
Checks if the result of `apply_rule` is in the memoization table according to the neighbourhood, `n`,

0 commit comments

Comments
 (0)