@@ -84,11 +84,16 @@ def evolve(cellular_automaton, timesteps, apply_rule, r=1, memoize=False):
84
84
85
85
:param r: the neighbourhood radius; the neighbourhood size will be 2r + 1 (default is 1)
86
86
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)
92
97
93
98
:return: a matrix, containing the results of the evolution, where the number of rows equal the number of time steps
94
99
specified
@@ -119,7 +124,7 @@ def _evolve_fixed(cellular_automaton, timesteps, apply_rule, r, memoize):
119
124
120
125
:param r: the neighbourhood radius; the neighbourhood size will be 2r + 1
121
126
122
- :param memoize: whether to use memoization
127
+ :param memoize: the memoization flag; one of True, False, or "recursive"
123
128
124
129
:return: a matrix, containing the results of the evolution, where the number of rows equal the number of time steps
125
130
specified
@@ -128,17 +133,24 @@ def _evolve_fixed(cellular_automaton, timesteps, apply_rule, r, memoize):
128
133
_ , cols = cellular_automaton .shape
129
134
array = np .zeros ((timesteps , cols ), dtype = cellular_automaton .dtype )
130
135
array [0 ] = initial_conditions
136
+ cell_indices = list (range (len (initial_conditions )))
131
137
132
138
memo_table = {}
133
139
134
140
for t in range (1 , timesteps ):
135
141
cells = array [t - 1 ]
136
142
strides = _index_strides (np .arange (len (cells )), 2 * r + 1 )
137
143
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 :
139
149
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 :
141
151
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 )
142
154
143
155
return np .concatenate ((cellular_automaton , array [1 :]), axis = 0 )
144
156
@@ -164,14 +176,15 @@ def _evolve_dynamic(cellular_automaton, timesteps, apply_rule, r, memoize):
164
176
165
177
:param r: the neighbourhood radius; the neighbourhood size will be 2r + 1
166
178
167
- :param memoize: whether to use memoization
179
+ :param memoize: the memoization flag; one of True, False, or "recursive"
168
180
169
181
:return: a matrix, containing the results of the evolution, where the number of rows equal the number of time steps
170
182
specified
171
183
"""
172
184
initial_conditions = cellular_automaton [- 1 ]
173
185
_ , cols = cellular_automaton .shape
174
186
array = [initial_conditions ]
187
+ cell_indices = list (range (len (initial_conditions )))
175
188
176
189
memo_table = {}
177
190
@@ -180,16 +193,89 @@ def _evolve_dynamic(cellular_automaton, timesteps, apply_rule, r, memoize):
180
193
cells = array [- 1 ]
181
194
strides = _index_strides (np .arange (len (cells )), 2 * r + 1 )
182
195
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 :
184
200
result = [_get_memoized (n , c , t , apply_rule , memo_table ) for c , n in enumerate (neighbourhoods )]
185
- else :
201
+ elif memoize is False :
186
202
result = [apply_rule (n , c , t ) for c , n in enumerate (neighbourhoods )]
203
+ else :
204
+ raise Exception ("unsupported memoization option: %s" % memoize )
187
205
array .append (np .array (result , dtype = cellular_automaton .dtype ))
188
206
t += 1
189
207
190
208
return np .concatenate ((cellular_automaton , array [1 :]), axis = 0 )
191
209
192
210
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
+
193
279
def _get_memoized (n , c , t , apply_rule , memoization_table ):
194
280
"""
195
281
Checks if the result of `apply_rule` is in the memoization table according to the neighbourhood, `n`,
@@ -446,6 +532,119 @@ def __call__(self, n, c, t):
446
532
raise NotImplementedError
447
533
448
534
535
+ class NKSRule (BaseRule ):
536
+ """
537
+ An Elementary Cellular Automaton rule, indexed according the scheme in NKS.
538
+ """
539
+ def __init__ (self , nks_rule_number ):
540
+ """
541
+ Creates an instance of an NKS rule.
542
+
543
+ :param nks_rule_number: an int indicating the cellular automaton rule number
544
+ """
545
+ self ._nks_rule_number = nks_rule_number
546
+
547
+ def __call__ (self , n , c , t ):
548
+ """
549
+ The NKS rule to apply.
550
+
551
+ :param n: a binary array of length 2r + 1
552
+
553
+ :param c: the index of the current cell
554
+
555
+ :param t: the current timestep
556
+
557
+ :return: the result, 0 or 1, of applying the given rule on the given state
558
+ """
559
+ return nks_rule (n , self ._nks_rule_number )
560
+
561
+
562
+ class BinaryRule (BaseRule ):
563
+ """
564
+ A binary representation of the given rule number, which is used to determine the value to return.
565
+ The process is approximately described as:
566
+
567
+ .. code-block:: text
568
+
569
+ 1. convert state to int, so [1,0,1] -> 5, call this state_int
570
+
571
+ 2. convert rule to binary, so 254 -> [1,1,1,1,1,1,1,0], call this rule_bin_array
572
+
573
+ 3. new value is rule_bin_array[7 - state_int]
574
+ we subtract 7 from state_int to be consistent with the numbering scheme used in NKS
575
+ in NKS, rule 254 for a 1D binary cellular automaton is described as:
576
+
577
+ [1,1,1] [1,1,0] [1,0,1] [1,0,0] [0,1,1] [0,1,0] [0,0,1] [0,0,0]
578
+ 1 1 1 1 1 1 1 0
579
+
580
+ If None is provided for the scheme parameter, the neighbourhoods are listed in lexicographic order (the reverse of
581
+ the NKS convention). If 'nks' is provided for the scheme parameter, the NKS convention is used for listing the
582
+ neighbourhoods.
583
+ """
584
+ def __init__ (self , rule , scheme = None , powers_of_two = None ):
585
+ """
586
+ Creates an instance of a binary rule.
587
+
588
+ :param rule: an int or a binary array indicating the cellular automaton rule number
589
+
590
+ :param scheme: can be None (default) or 'nks'; if 'nks' is given, the rule numbering scheme used in NKS is used
591
+
592
+ :param powers_of_two: a pre-computed array containing the powers of two, e.g. [4,2,1]; can be None (default) or
593
+ an array of length len(neighbourhood); if an array is given, it will used to speed up the
594
+ calculation of state_int
595
+ """
596
+ self ._rule = rule
597
+ self ._scheme = scheme
598
+ self ._powers_of_two = powers_of_two
599
+
600
+ def __call__ (self , n , c , t ):
601
+ """
602
+ The binary rule to apply.
603
+
604
+ :param n: a binary array of length 2r + 1
605
+
606
+ :param c: the index of the current cell
607
+
608
+ :param t: the current timestep
609
+
610
+ :return: the result, 0 or 1, of applying the given rule on the given state
611
+ """
612
+ return binary_rule (n , self ._rule , self ._scheme , self ._powers_of_two )
613
+
614
+
615
+ class TotalisticRule (BaseRule ):
616
+ """
617
+ The totalistic rule as described in NKS. The average color is mapped to a whole number in [0, k - 1].
618
+ The rule number is in base 10, but interpreted in base k. For a 1-dimensional cellular automaton, there are
619
+ 3k - 2 possible average colors in the 3-cell neighbourhood. There are n(k - 1) + 1 possible average colors for a
620
+ k-color cellular automaton with an n-cell neighbourhood.
621
+ """
622
+ def __init__ (self , k , rule ):
623
+ """
624
+ Creates an instance of a totalistic rule.
625
+
626
+ :param k: the number of colors in this cellular automaton, where only 2 <= k <= 36 is supported
627
+
628
+ :param rule: the k-color cellular automaton rule number in base 10, interpreted in base k
629
+ """
630
+ self ._k = k
631
+ self ._rule = rule
632
+
633
+ def __call__ (self , n , c , t ):
634
+ """
635
+ The totalistic rule to apply.
636
+
637
+ :param n: a k-color array of any size
638
+
639
+ :param c: the index of the current cell
640
+
641
+ :param t: the current timestep
642
+
643
+ :return: the result, a number from 0 to k - 1, of applying the given rule on the given state
644
+ """
645
+ return totalistic_rule (n , self ._k , self ._rule )
646
+
647
+
449
648
class ReversibleRule (BaseRule ):
450
649
"""
451
650
An elementary cellular automaton rule explicitly set up to be reversible.
0 commit comments