Removed Cell class and use ints instead
parent
a0f62d9d4c
commit
0236418ef1
|
@ -2,15 +2,15 @@ import cam
|
|||
import ruleset as rs
|
||||
import neighborhood as nh
|
||||
|
||||
def game_of_life(cell, neighbors):
|
||||
def game_of_life(coordinate, grid, neighbors):
|
||||
"""
|
||||
Rules of the Game of Life.
|
||||
|
||||
Note we ignore the second component of the neighbors tuples since
|
||||
life depends on all neighbors
|
||||
"""
|
||||
total = sum(map(lambda x: int(x[0].value), neighbors))
|
||||
if cell.value:
|
||||
total = sum(map(lambda x: x[1], neighbors))
|
||||
if grid[coordinate]:
|
||||
if total < 2 or total > 3:
|
||||
return False
|
||||
else:
|
||||
|
@ -24,5 +24,5 @@ if __name__ == '__main__':
|
|||
c = cam.CAM(1, (100, 100))
|
||||
c.randomize()
|
||||
r = rs.Ruleset(rs.Rule.SATISFY)
|
||||
n = nh.Neighborhood.moore(c.master.grid, True)
|
||||
c.start_plot(50, r, n, game_of_life)
|
||||
n = nh.Neighborhood.moore(c.master, True)
|
||||
c.start_plot(100, r, n, game_of_life)
|
||||
|
|
31
src/cam.py
31
src/cam.py
|
@ -1,12 +1,13 @@
|
|||
"""
|
||||
|
||||
|
||||
@author: jrpotter
|
||||
@date: June 01, 2015
|
||||
"""
|
||||
import time
|
||||
import copy
|
||||
|
||||
import camtools
|
||||
import ruleset as rs
|
||||
import cell_plane as cp
|
||||
import neighborhood as nh
|
||||
|
||||
import numpy as np
|
||||
|
@ -23,14 +24,12 @@ class CAM:
|
|||
all methods needed (i.e. supported) to interact/configure the cellular automata as desired.
|
||||
"""
|
||||
|
||||
def __init__(self, cps=1, dimen=(100,100)):
|
||||
def __init__(self, cps=1, dimen=(100, 100)):
|
||||
"""
|
||||
|
||||
"""
|
||||
cps = max(cps, 1)
|
||||
self._dimen = dimen
|
||||
self._planes = [cp.CellPlane(dimen) for i in range(cps)]
|
||||
self.master = self._planes[0]
|
||||
self.planes = np.zeros((max(cps, 1),) + dimen, dtype=np.int32)
|
||||
self.master = self.planes[0]
|
||||
|
||||
|
||||
def start_plot(self, clock, ruleset, neighborhood, *args):
|
||||
|
@ -46,11 +45,11 @@ class CAM:
|
|||
ax.get_xaxis().set_visible(False)
|
||||
ax.get_yaxis().set_visible(False)
|
||||
|
||||
mshown = plt.matshow(self.master.to_binary(), fig.number)
|
||||
mshown = plt.matshow(self.master, fig.number)
|
||||
|
||||
def animate(frame):
|
||||
self.tick(ruleset, neighborhood, *args)
|
||||
mshown.set_array(self.master.to_binary())
|
||||
mshown.set_array(self.master)
|
||||
fig.canvas.draw()
|
||||
|
||||
ani.FuncAnimation(fig, animate, interval=clock)
|
||||
|
@ -64,7 +63,7 @@ class CAM:
|
|||
|
||||
"""
|
||||
while True:
|
||||
print(self.to_binary())
|
||||
print(self.master)
|
||||
time.sleep(clock / 1000)
|
||||
self.tick(ruleset, neighborhood, *args)
|
||||
|
||||
|
@ -77,17 +76,15 @@ class CAM:
|
|||
is placed into the master grid. Depending on the timing specifications set by the user, this
|
||||
may also change secondary cell planes (the master is always updated on each tick).
|
||||
"""
|
||||
self.master.grid[:] = rs.Ruleset.update(self.master.grid, ruleset, neighborhood, *args)
|
||||
tmp = np.copy(self.master)
|
||||
for i in range(len(self.master.flat)):
|
||||
tmp.flat[i] = ruleset.call(i, self.master, neighborhood, *args)
|
||||
self.master[:] = tmp
|
||||
|
||||
|
||||
def randomize(self):
|
||||
"""
|
||||
Set the master grid to a random configuration.
|
||||
"""
|
||||
@np.vectorize
|
||||
def v_random(cell):
|
||||
cell.value = np.random.random_integers(0, 1)
|
||||
return cell
|
||||
|
||||
v_random(self.master.grid)
|
||||
self.master[:] = np.random.random_integers(0, 1, self.master.shape)
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
"""
|
||||
|
||||
@author: jrpotter
|
||||
@date: June 01, 2015
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
def flatten(coordinates, grid):
|
||||
"""
|
||||
Given the coordinates of a matrix, returns the index of the flat matrix.
|
||||
"""
|
||||
index = 0
|
||||
for i in range(len(coordinates)):
|
||||
index += coordinates[i] * np.prod(grid.shape[i+1:], dtype=np.int32)
|
||||
|
||||
return index
|
||||
|
||||
|
||||
def unflatten(index, grid):
|
||||
"""
|
||||
Given an index of a flat matrix, returns the corresponding coordinates.
|
||||
"""
|
||||
coordinates = []
|
||||
for i in range(len(grid.shape)):
|
||||
tmp = np.prod(grid.shape[i+1:], dtype=np.int32)
|
||||
coordinates.append(index // tmp)
|
||||
index -= tmp * coordinates[-1]
|
||||
|
||||
return tuple(coordinates)
|
||||
|
||||
|
||||
def comp_add(coor1, coor2):
|
||||
"""
|
||||
Adds components of coordinates element-wise.
|
||||
"""
|
||||
return tuple(map(sum, zip(coor1, coor2)))
|
|
@ -1,69 +0,0 @@
|
|||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
class Cell:
|
||||
"""
|
||||
Represents a "cell" in a CellPlane.
|
||||
|
||||
Note we keep track of the index for vectorization purposes. By maintaining each index
|
||||
and batch updating via the given index, we can much more efficiently update the entire
|
||||
cell plane.
|
||||
"""
|
||||
def __init__(self, value, *index):
|
||||
self.value = value
|
||||
self.index = index
|
||||
|
||||
|
||||
class CellPlane:
|
||||
"""
|
||||
A CellPlane represents a layer of the grids that can be placed on top of one another in a 2D CAM.
|
||||
|
||||
The use of multiple cell plane allow for more intricate states of life and death, though there
|
||||
exists only a single master cell plane that controls the others. That is, the master cell plane has
|
||||
a CAM ruleset applied to it, and the other cell planes merely copy the master, though this can
|
||||
be delayed and have different color mappings.
|
||||
|
||||
For example, by setting a delay of two ticks on the second cell plane of a 2-level CAM configuration,
|
||||
one can allow for ECHOing, providing a more intuitive sense of "velocity" based on the master.
|
||||
|
||||
That is not to say one could not have multiple CAM's operating simultaneously though. We can consider
|
||||
a configuration to consist of an arbitrary number of planes, of which one is the master, but multiple
|
||||
masters can exist in separate CAMs that can interact with one another.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@np.vectorize
|
||||
def _populate(*indices):
|
||||
"""
|
||||
The following joins indices in N-dimensions together.
|
||||
|
||||
This information is stored in a cell (with initial value False) in order for batch processing
|
||||
to be performed when actually updating values and computing whether a cell is on or off. For
|
||||
example, if exploring a 4D array, we want to be able to know which cells we need to check the
|
||||
status of, but this is relative to the current cell, whose position we do not know unless that
|
||||
information is stored with the current cell.
|
||||
"""
|
||||
return Cell(False, *indices)
|
||||
|
||||
|
||||
def __init__(self, dimen):
|
||||
"""
|
||||
|
||||
"""
|
||||
self.grid = CellPlane._populate(*np.indices(dimen))
|
||||
|
||||
|
||||
def to_binary(self):
|
||||
"""
|
||||
|
||||
"""
|
||||
vfunc = np.vectorize(lambda x: int(x.value))
|
||||
return vfunc(self.grid)
|
||||
|
|
@ -4,7 +4,8 @@
|
|||
@author: jrpotter
|
||||
@date: May 31st, 2015
|
||||
"""
|
||||
import itertools
|
||||
import camtools as ct
|
||||
import itertools as it
|
||||
|
||||
|
||||
class Neighborhood:
|
||||
|
@ -23,45 +24,7 @@ class Neighborhood:
|
|||
at this point isn't possible.
|
||||
"""
|
||||
|
||||
class NeighborhoodKey:
|
||||
"""
|
||||
Allows proper sorting of neighborhoods.
|
||||
|
||||
Lists should be returned in order, where cell's with smaller indices (in most significant axis first)
|
||||
are listed before cell's with larger ones. For example, in a 3D grid, the neighbors corresponding to:
|
||||
|
||||
offsets = (-1, -1, -1), (-1, 1, 0), (-1, 0, -1), and (1, 0, -1)
|
||||
|
||||
are returned in the following order:
|
||||
|
||||
offsets = (-1, -1, -1), (-1, 0, -1), (1, 0, -1), (-1, 1, 0)
|
||||
|
||||
since the z-axis is most significant, followed by the y-axis, and lastly the x-axis.
|
||||
"""
|
||||
def __init__(self, obj, *args):
|
||||
self.obj = obj
|
||||
def __lt__(self, other):
|
||||
return self.compare(self.obj, other.obj) < 0
|
||||
def __gt__(self, other):
|
||||
return self.compare(self.obj, other.obj) > 0
|
||||
def __eq__(self, other):
|
||||
return self.compare(self.obj, other.obj) == 0
|
||||
def __le__(self, other):
|
||||
return self.compare(self.obj, other.obj) <= 0
|
||||
def __ge__(self, other):
|
||||
return self.compare(self.obj, other.obj) >= 0
|
||||
def __ne__(self, other):
|
||||
return self.compare(self.obj, other.obj) != 0
|
||||
def compare(self, other):
|
||||
for i in reversed(range(len(a))):
|
||||
if a[i] < b[i]:
|
||||
return -1
|
||||
elif a[i] > b[i]:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def __init__(self, grid, wrap_around=True):
|
||||
def __init__(self, wrap_around=True):
|
||||
"""
|
||||
Sets up an empty neighborhood.
|
||||
|
||||
|
@ -69,42 +32,41 @@ class Neighborhood:
|
|||
Note the offsets have a tuple as a key representing the position being offsetted by, and as a value,
|
||||
the current state the given cell at the offset is checked to be.
|
||||
"""
|
||||
self.grid = grid
|
||||
self.offsets = {}
|
||||
self.wrap_around = wrap_around
|
||||
|
||||
|
||||
def neighbors(self, cell):
|
||||
def neighbors(self, index, grid):
|
||||
"""
|
||||
Returns all cells in the given neighborhood.
|
||||
|
||||
The returned cells are grouped with the value the cell is checked to be (a 2-tuple (Cell, value) pair).
|
||||
These are sorted based on the NeighborhoodKey comparison class defined above.
|
||||
The returned list of indices represent the index in question, the value at the given index, and
|
||||
the expected value as defined in the offsets.
|
||||
"""
|
||||
cells = []
|
||||
for k in sorted(self.offsets.keys()):
|
||||
position = [sum(x) for x in zip(cell.index, k)]
|
||||
for i in range(len(position)):
|
||||
indices = []
|
||||
for key in sorted(self.offsets.keys()):
|
||||
if self.wrap_around:
|
||||
position[i] = position[i] % self.grid.shape[i]
|
||||
elif i < 0 or i >= self.grid.shape[i]:
|
||||
break
|
||||
f_index = (key + index) % len(grid.flat)
|
||||
indices.append((f_index, grid.flat[f_index], self.offsets[key]))
|
||||
else:
|
||||
cells.append((self.grid[tuple(position)], self.offsets[k]))
|
||||
pass
|
||||
|
||||
return cells
|
||||
return indices
|
||||
|
||||
|
||||
def extend(self, offsets, strict=False):
|
||||
def extend(self, offsets, grid, strict=False):
|
||||
"""
|
||||
Adds new offsets to the instance member offsets.
|
||||
|
||||
We complain if the strict flag is set to True and an offset has already been declared with a different value.
|
||||
Note also that all offsets are indices of the flattened matrix. This allows for quick row indexing as opposed
|
||||
to individual coordinates.
|
||||
"""
|
||||
f_offsets = {ct.flatten(k, grid): v for k, v in offsets.items()}
|
||||
if not strict:
|
||||
self.offsets.update(offsets)
|
||||
self.offsets.update(f_offsets)
|
||||
else:
|
||||
for k in offsets.keys():
|
||||
for k in f_offsets.keys():
|
||||
value = self.offsets.get(k, None)
|
||||
if value is None:
|
||||
self.offsets[k] = offsets[k]
|
||||
|
@ -125,12 +87,12 @@ class Neighborhood:
|
|||
"""
|
||||
offsets = {}
|
||||
variants = ([-1, 0, 1],) * len(grid.shape)
|
||||
for current in itertools.product(*variants):
|
||||
for current in it.product(*variants):
|
||||
if any(current):
|
||||
offsets[current] = value
|
||||
|
||||
m_neighborhood = cls(grid, wrap_around)
|
||||
m_neighborhood.extend(offsets)
|
||||
m_neighborhood = cls(wrap_around)
|
||||
m_neighborhood.extend(offsets, grid)
|
||||
|
||||
return m_neighborhood
|
||||
|
||||
|
@ -154,8 +116,8 @@ class Neighborhood:
|
|||
offsets[tuple(variant)] = value
|
||||
variant[i] = 0
|
||||
|
||||
n_neighborhood = cls(grid, wrap_around)
|
||||
n_neighborhood.extend(offsets)
|
||||
n_neighborhood = cls(wrap_around)
|
||||
n_neighborhood.extend(offsets, grid)
|
||||
|
||||
return n_neighborhood
|
||||
|
||||
|
|
|
@ -5,9 +5,8 @@
|
|||
@author: jrpotter
|
||||
@date: May 31st, 2015
|
||||
"""
|
||||
import copy
|
||||
import enum
|
||||
import numpy as np
|
||||
import camtools
|
||||
|
||||
|
||||
class Rule(enum.Enum):
|
||||
|
@ -29,26 +28,6 @@ class Ruleset:
|
|||
a neighborhood instance's offsets member.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@np.vectorize
|
||||
def update(cell, rules, neighborhood, *args):
|
||||
"""
|
||||
Allow for batch processing of rules.
|
||||
|
||||
We choose our processing function based on the specified rule and update every cell in the grid simultaneously
|
||||
via a vectorization.
|
||||
"""
|
||||
tmp = copy.deepcopy(cell)
|
||||
if rules.method == Rule.MATCH:
|
||||
tmp.value = rules.matches(cell, neighborhood, *args)
|
||||
elif rules.method == Rule.TOLERATE:
|
||||
tmp.value = rules.tolerate(cell, neighborhood, *args)
|
||||
elif rules.method == Rule.SATISFY:
|
||||
tmp.value = rules.satisfies(cell, neighborhood, *args)
|
||||
|
||||
return tmp
|
||||
|
||||
|
||||
def __init__(self, method):
|
||||
"""
|
||||
|
||||
|
@ -56,22 +35,22 @@ class Ruleset:
|
|||
self.method = method
|
||||
|
||||
|
||||
def matches(self, cell, neighborhood):
|
||||
def matches(self, index, grid, neighborhood):
|
||||
"""
|
||||
Determines that neighborhood matches expectation exactly.
|
||||
|
||||
Note this is just like the tolerate method with a tolerance of 1, but
|
||||
recoding allows for short circuiting.
|
||||
"""
|
||||
residents = neighborhood.neighbors(cell)
|
||||
residents = neighborhood.neighbors(index, grid)
|
||||
for resident in residents:
|
||||
if resident[0].value != resident[1]:
|
||||
if grid[resident[0]] != resident[1]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def tolerate(self, cell, neighborhood, tolerance):
|
||||
def tolerate(self, index, grid, neighborhood, tolerance):
|
||||
"""
|
||||
Determines that neighborhood matches expectation within tolerance.
|
||||
|
||||
|
@ -79,23 +58,40 @@ class Ruleset:
|
|||
consider this cell to be alive. Note tolerance must be a value 0 <= t <= 1.
|
||||
"""
|
||||
matches = 0
|
||||
residents = neighborhood.neighbors(cell)
|
||||
residents = neighborhood.neighbors(index, grid)
|
||||
for resident in residents:
|
||||
if resident[0].value == resident[1]:
|
||||
if grid[resident[0]] == resident[1]:
|
||||
matches += 1
|
||||
|
||||
return (matches / len(residents)) >= tolerance
|
||||
|
||||
|
||||
def satisfies(self, cell, neighborhood, valid_func):
|
||||
def satisfies(self, index, grid, neighborhood, valid_func):
|
||||
"""
|
||||
Allows custom function to relay next state of given cell.
|
||||
|
||||
The passed function is supplied the list of 2-tuple elements, of which the first is a Cell and the second is
|
||||
the expected state as declared in the Neighborhood, as well as the grid and cell in question.
|
||||
"""
|
||||
residents = neighborhood.neighbors(cell)
|
||||
residents = neighborhood.neighbors(index, grid)
|
||||
coordinate = camtools.unflatten(index, grid)
|
||||
|
||||
return valid_func(cell, residents)
|
||||
return valid_func(coordinate, grid, residents)
|
||||
|
||||
|
||||
def call(self, index, grid, neighborhood, *args):
|
||||
"""
|
||||
Allow for batch processing of rules.
|
||||
|
||||
We choose our processing function based on the specified rule and update every cell in the grid simultaneously
|
||||
via a vectorization.
|
||||
"""
|
||||
if self.method == Rule.MATCH:
|
||||
func = self.matches
|
||||
elif self.method == Rule.TOLERATE:
|
||||
func = self.tolerate
|
||||
elif self.method == Rule.SATISFY:
|
||||
func = self.satisfies
|
||||
|
||||
return int(func(index, grid, neighborhood, *args))
|
||||
|
||||
|
|
Loading…
Reference in New Issue