Neighborhood logic established. Optimization needed
parent
3917627234
commit
cacbfe29ca
42
README.md
42
README.md
|
@ -7,11 +7,41 @@ though mentioned as reasonably priced, a CAM Forth machine is out of my price ra
|
||||||
|
|
||||||
The following uses numpy/matplotlib underneath, and will ideally incorporate the following:
|
The following uses numpy/matplotlib underneath, and will ideally incorporate the following:
|
||||||
|
|
||||||
* Arbitrary description of neighborhoods
|
* N-Dimensional Cellular Automata
|
||||||
* Arbitrary leveling of bit planes
|
* Arbitrary count of bit planes and description of neighborhoods
|
||||||
* Arbitrary description of rulesets
|
* Timing specifications and control for granular viewing
|
||||||
* 2D and 3D cellular automata
|
* ECHOing and TRACing in library for 2D CAMs
|
||||||
* Timing specifications for granular viewing
|
|
||||||
* Echoing and Tracing (for 2D)
|
Documentation will be made available at fuzzykayak.com/... but a quickstart will be provided below.
|
||||||
|
There are also a variety of examples given to demonstrate different means of building CAMS.
|
||||||
|
|
||||||
|
Quickstart
|
||||||
|
----------
|
||||||
|
|
||||||
|
To begin construction of a CAM, we need two objects: a CAM and a Ruleset.
|
||||||
|
|
||||||
|
A CAM can be broken down into a list of cell planes, each of which contain the same number of states.
|
||||||
|
Of these planes, the first is considered the master, and all others are mirrors of the master at an
|
||||||
|
earlier stage in time (this allows for methods such as ECHOing).
|
||||||
|
|
||||||
|
A ruleset can further be broken down into a list of configurations, of which one must pass
|
||||||
|
for the state of a cell to change. During application of a ruleset, each cell is described by
|
||||||
|
a neighborhood, which packages all other cells considered in the given plane.
|
||||||
|
|
||||||
|
The following will construct Conway's Game of Life, as shown in the provided GIF:
|
||||||
|
|
||||||
|
```
|
||||||
|
import cam
|
||||||
|
import ruleset as rs
|
||||||
|
|
||||||
|
# View the different formats the CAMParser can parse. Manual construction for
|
||||||
|
# more complicated rulesets are also a possibility
|
||||||
|
c = cam.CAM(1, 100, 2)
|
||||||
|
p = u.CAMParser('B3/S23', c)
|
||||||
|
|
||||||
|
# 400 represents the time, in milliseconds, before the next tick occurs
|
||||||
|
c.randomize()
|
||||||
|
c.start_plot(400, p.ruleset)
|
||||||
|
```
|
||||||
|
|
||||||
![alt tag](https://raw.githubusercontent.com/jrpotter/fifth/master/rsrc/demo.gif)
|
![alt tag](https://raw.githubusercontent.com/jrpotter/fifth/master/rsrc/demo.gif)
|
||||||
|
|
13
src/cam.py
13
src/cam.py
|
@ -1,11 +1,10 @@
|
||||||
"""
|
"""
|
||||||
Top level module representing a Cellular Automata Machine.
|
Top level module representing a Cellular Automata Machine.
|
||||||
|
|
||||||
The CAM consists of any number of cell planes that allow for increasingly complex cellular automata.
|
The CAM consists of a number of cell planes that allow for increasingly complex cellular automata.
|
||||||
This is the top-level module that should be used by anyone wanting to work with fifth, and provides
|
This is the top-level module that should be used by anyone wanting to work with fifth, and provides
|
||||||
all methods needed (i.e. supported) to interact/configure the cellular automata as desired.
|
all methods needed (i.e. supported) to interact/configure with the cellular automata directly.
|
||||||
|
|
||||||
@author: jrpotter
|
|
||||||
@date: June 01, 2015
|
@date: June 01, 2015
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
|
@ -22,9 +21,9 @@ class CAM:
|
||||||
directly, but instead mirror the master plane, and reflect these changes after a given number of
|
directly, but instead mirror the master plane, and reflect these changes after a given number of
|
||||||
"ticks."
|
"ticks."
|
||||||
|
|
||||||
A tick represents an interval of time after which all states should be updated, and, therefore, all
|
A tick represents an interval of time after which all states of a given set of cell planes should be
|
||||||
cell planes should be updated. Certain planes may or may not change every tick, but instead on every
|
updated. should be updated, Certain planes may or may not change every tick, but instead on every
|
||||||
nth tick, allowing for more sophisticated views such as ECHOing and TRACE-ing.
|
nth tick, allowing for more sophisticated views such as ECHOing and TRACing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cps=1, states=100, dimen=2):
|
def __init__(self, cps=1, states=100, dimen=2):
|
||||||
|
@ -49,7 +48,7 @@ class CAM:
|
||||||
The tick function should be called whenever we want to change the current status of the grid.
|
The tick function should be called whenever we want to change the current status of the grid.
|
||||||
Every time the tick is called, the ruleset is applied to each cell and the next set of states
|
Every time the tick is called, the ruleset is applied to each cell and the next set of states
|
||||||
is placed into the master grid. Depending on the timing specifications set by the user, this
|
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).
|
may also change secondary cell planes (the master, by default, is always updated on each tick).
|
||||||
"""
|
"""
|
||||||
self.total += 1
|
self.total += 1
|
||||||
for i, j in self.ticks:
|
for i, j in self.ticks:
|
||||||
|
|
|
@ -8,7 +8,7 @@ class Neighborhood:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, index, offsets):
|
def __init__(self, f_index, b_offset, states, indices):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
119
src/ruleset.py
119
src/ruleset.py
|
@ -4,11 +4,9 @@ The following determines the next state of a given cell in a CAM.
|
||||||
The ruleset takes in a collection of rules specifying neighborhoods, as well as the configurations of
|
The ruleset takes in a collection of rules specifying neighborhoods, as well as the configurations of
|
||||||
said neighborhood that yield an "on" or "off" state on the cell a ruleset is being applied to.
|
said neighborhood that yield an "on" or "off" state on the cell a ruleset is being applied to.
|
||||||
|
|
||||||
@author: jrpotter
|
|
||||||
@date: May 31st, 2015
|
@date: May 31st, 2015
|
||||||
"""
|
"""
|
||||||
import enum
|
import enum
|
||||||
import itertools as it
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
@ -27,19 +25,20 @@ class Ruleset:
|
||||||
|
|
||||||
must match exactly for the center cell to be a 1, then each cell is checked for this configuration, and its
|
must match exactly for the center cell to be a 1, then each cell is checked for this configuration, and its
|
||||||
state is updated afterward (note the above is merely for clarity; a configuration is not defined as such). Note
|
state is updated afterward (note the above is merely for clarity; a configuration is not defined as such). Note
|
||||||
configurations are checked until a match occurs, in a FIFO manner.
|
configurations are checked until a match occurs, in order of the configurations list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Method(enum.Enum):
|
class Method(enum.Enum):
|
||||||
"""
|
"""
|
||||||
Specifies how a ruleset should be applied to a given cell.
|
Specifies how a ruleset should be applied to a given cell.
|
||||||
|
|
||||||
* A match declares that a given configuration must match exactly for the cell to be considered on.
|
* A match declares that a given configuration must match exactly for a configuration to pass
|
||||||
* A tolerance specifies that a configuration must match within a given percentage to be considered on.
|
* A tolerance specifies that a configuration must match within a given percentage to pass
|
||||||
* A specification allows the user to define a custom function which must return a boolean, declaring
|
* A specification allows the user to define a custom function which must return a boolean, declaring
|
||||||
whether a cell should be on or off. This function is given the current cell's state, as well as
|
whether a configuration passes. This function is given a neighborhood with all necessary information.
|
||||||
the state of the cell's neighbors.
|
* Always passing allows the first configuration to always yield a success. It is redundant to add
|
||||||
|
any additional configurations in this case (in fact it is inefficient since neighborhoods are computer
|
||||||
|
in advance).
|
||||||
"""
|
"""
|
||||||
MATCH = 0
|
MATCH = 0
|
||||||
TOLERATE = 1
|
TOLERATE = 1
|
||||||
|
@ -48,66 +47,24 @@ class Ruleset:
|
||||||
|
|
||||||
def __init__(self, method):
|
def __init__(self, method):
|
||||||
"""
|
"""
|
||||||
@grid: Every ruleset is bound to a grid, which a ruleset is applied to.
|
A ruleset does not begin with any configurations; only a means of verifying them.
|
||||||
@method: One of the values defined in the RulesetMethod enumeration. View class for description.
|
|
||||||
|
@method: One of the values defined in the Ruleset.Method enumeration. View class for description.
|
||||||
"""
|
"""
|
||||||
self.method = method
|
self.method = method
|
||||||
self.configurations = []
|
self.configurations = []
|
||||||
|
|
||||||
def addConfiguration(self, grid, next_state, offsets):
|
|
||||||
"""
|
|
||||||
Creates a configuration and saves said configuration.
|
|
||||||
"""
|
|
||||||
config = Configuration(grid, next_state, offsets)
|
|
||||||
self.configurations.append(config)
|
|
||||||
|
|
||||||
def applyTo(self, plane, *args):
|
def applyTo(self, plane, *args):
|
||||||
"""
|
"""
|
||||||
Depending on the set method, applies ruleset to each cell in the plane.
|
Depending on the set method, applies ruleset to each cell in the plane.
|
||||||
|
|
||||||
Note we first compute all neighborhoods in a batch manner and then test that a configuration
|
|
||||||
passes on the supplied neighborhood.
|
|
||||||
|
|
||||||
@args: If our method is TOLERATE, we pass in a value in set [0, 1]. This specifies the threshold between a
|
@args: If our method is TOLERATE, we pass in a value in set [0, 1]. This specifies the threshold between a
|
||||||
passing (i.e. percentage of matches in a configuration is > arg) and failing. If our method is SATISFY,
|
passing (i.e. percentage of matches in a configuration is > arg) and failing. If our method is SATISFY,
|
||||||
arg should be a function returning a BOOL, which takes in a current cell's value, and the
|
arg should be a function returning a BOOL, which takes in a current cell's value, and the
|
||||||
value of its neighbors.
|
value of its neighbors.
|
||||||
"""
|
"""
|
||||||
master = plane.grid.flat
|
|
||||||
|
|
||||||
for config in self.configurations:
|
|
||||||
|
|
||||||
# Construct neighborhoods
|
|
||||||
#
|
|
||||||
# After profiling with a previous version, I found that going through each index and totaling the number
|
|
||||||
# of active states was taking much longer than I liked. Instead, we compute as many neighborhoods as possible
|
|
||||||
# simultaneously, avoiding explicit summation via the "sum" function, at least for each state separately.
|
|
||||||
#
|
|
||||||
# Because the states are now represented as numbers, we instead convert each number to their binary representation
|
|
||||||
# and add the binary representations together. We do this in chunks of 9, depending on the number of offsets, so
|
|
||||||
# no overflowing of a single column can occur. We can then find the total of the ith neighborhood by checking the
|
|
||||||
# sum of the ith index of the summation of every 9 chunks of numbers (this is done a row at a time).
|
|
||||||
|
|
||||||
# TODO: Config offsets should be flat index, bit offset
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
neighborhoods = []
|
|
||||||
|
|
||||||
values = []
|
|
||||||
for f_index, offset in config.offsets:
|
|
||||||
val = plane.f_bits([f_index])
|
|
||||||
values.append(int(val[offset+1:] + val[:offset]))
|
|
||||||
|
|
||||||
# Chunk into groups of 9 and sum all values
|
|
||||||
chunks = [values[i:i+9] for i in range(0, len(values), 9)]
|
|
||||||
summands = map(sum, chunks)
|
|
||||||
|
|
||||||
# Construct neighborhoods for each value in list
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine which function should be used to test success
|
||||||
if self.method == Ruleset.Method.MATCH:
|
if self.method == Ruleset.Method.MATCH:
|
||||||
vfunc = self._matches
|
vfunc = self._matches
|
||||||
elif self.method == Ruleset.Method.TOLERATE:
|
elif self.method == Ruleset.Method.TOLERATE:
|
||||||
|
@ -117,13 +74,57 @@ class Ruleset:
|
||||||
elif self.method == Ruleset.Method.ALWAYS_PASS:
|
elif self.method == Ruleset.Method.ALWAYS_PASS:
|
||||||
vfunc = lambda *args: True
|
vfunc = lambda *args: True
|
||||||
|
|
||||||
# Apply the function if possible
|
# Find the set of neighborhoods for each given configuration
|
||||||
passed, state = config.passes(f_index, grid, vfunc, *args)
|
neighborhoods = [self._construct_neighborhoods(plane, config) for c in self.configurations]
|
||||||
|
for f_idx, value in enumerate(self.plane.flat):
|
||||||
|
for b_offset in len(self.plane.shape[-1]):
|
||||||
|
for c_idx, config in enumerate(self.configurations):
|
||||||
|
n_idx = f_idx * self.plane.shape[-1] + b_offset
|
||||||
|
passed, state = config.passes(neighborhoods[c_idx][n_idx], vfunc, *args)
|
||||||
if passed:
|
if passed:
|
||||||
return state
|
plane[f_idx][b_offset] = state
|
||||||
|
break
|
||||||
|
|
||||||
# If no configuration passes, we leave the state unchanged
|
def _construct_neighborhoods(self, plane, config):
|
||||||
return grid.flat[f_index]
|
"""
|
||||||
|
Construct neighborhoods
|
||||||
|
|
||||||
|
After profiling with a previous version, I found that going through each index and totaling the number
|
||||||
|
of active states was taking much longer than I liked. Instead, we compute as many neighborhoods as possible
|
||||||
|
simultaneously, avoiding explicit summation via the "sum" function, at least for each state separately.
|
||||||
|
|
||||||
|
Because the states are now represented as numbers, we instead convert each number to their binary representation
|
||||||
|
and add the binary representations together. We do this in chunks of 9, depending on the number of offsets, so
|
||||||
|
no overflowing of a single column can occur. We can then find the total of the ith neighborhood by checking the
|
||||||
|
sum of the ith index of the summation of every 9 chunks of numbers (this is done a row at a time).
|
||||||
|
|
||||||
|
TODO: Config offsets should be flat offset, bit offset
|
||||||
|
"""
|
||||||
|
neighborhoods = []
|
||||||
|
|
||||||
|
for f_idx, row in enumerate(plane.grid.flat):
|
||||||
|
|
||||||
|
# Construct the current neighborhoods of each bit beforehand
|
||||||
|
row_neighborhoods = [Neighborhood(f_idx, i) for i in range(plane.shape[-1])]
|
||||||
|
|
||||||
|
# Note: config's offsets contain the index of the number in the plane's flat iterator
|
||||||
|
# and the offset of the bit referring to the actual state in the given neighborhood
|
||||||
|
offset_totals = []
|
||||||
|
for f_offset, b_offset in config.offsets:
|
||||||
|
row_offset = plane.f_bits(f_idx + f_offset)
|
||||||
|
offset_totals.append(int(row_offset[b_offset+1:] + row_offset[:b_offset]))
|
||||||
|
|
||||||
|
# Chunk into groups of 9 and sum all values
|
||||||
|
# These summations represent the total number of states in a given neighborhood
|
||||||
|
chunks = map(sum, [offset_totals[i:i+9] for i in range(0, len(offset_totals), 9)])
|
||||||
|
for chunk in chunks:
|
||||||
|
for i in range(len(row_neighborhoods)):
|
||||||
|
row_neighborhoods[i].total += int(chunk[i])
|
||||||
|
|
||||||
|
# Lastly, keep neighborhoods 1D, to easily relate to the flat plane grid
|
||||||
|
neighborhoods += row_neighborhoods
|
||||||
|
|
||||||
|
return neighborhoods
|
||||||
|
|
||||||
def _matches(self, f_index, f_grid, indices, states):
|
def _matches(self, f_index, f_grid, indices, states):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue