Reworked CellularAutomaton to improve API and speed

+ Added CI
+ Restructured Project
+ Improved API Improved creation speed by factor of \~2
+ Improved execution speed by factor of \~15

- Removed multi processor since it doesn't work with the new setup and was not fast enough to matter.
This commit is contained in:
Richard Feistenauer 2020-10-20 10:14:05 +00:00
parent 4ab4b2a6d2
commit 9b527a044f
31 changed files with 801 additions and 1154 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ dist/
cellular_automaton.egg-info/ cellular_automaton.egg-info/
build/ build/
MANIFEST MANIFEST
timing.log

23
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,23 @@
image: "python:3.7"
before_script:
- python --version
- pip install coverage pytest pytest-cov pylint recordclass coverage coverage-badge
stages:
- Static Analysis
- Test
pylint:
stage: Static Analysis
script:
- pylint -d C0301 cellular_automaton/*.py
test:
stage: Test
script:
- pytest --cov=cellular_automaton tests/
- coverage report -m
- coverage-badge
coverage: '/TOTAL.+ ([0-9]{1,3}%)/'

10
.pylintrc Normal file
View File

@ -0,0 +1,10 @@
[MASTER]
jobs=4
[REFACTORING]
max-nested-blocks=2
[BASIC]
docstring-min-length=2
good-names=i, j, k, n, _, x, y
max-line-length=120

View File

@ -11,7 +11,6 @@ It is not the first python module to provide a cellular automaton,
but it is to my best knowledge the first that provides all of the following features: but it is to my best knowledge the first that provides all of the following features:
- easy to use - easy to use
- n dimensional - n dimensional
- multi process capable
- speed optimized - speed optimized
- documented - documented
- tested - tested
@ -27,20 +26,44 @@ to have a clean and tested code with a decent coverage added some more requireme
The speed optimization and multi process capability was more of challenge for myself. The speed optimization and multi process capability was more of challenge for myself.
IMHO the module now reached an acceptable speed, but there is still room for improvements (e.g. with Numba?). IMHO the module now reached an acceptable speed, but there is still room for improvements (e.g. with Numba?).
### Changelog
#### 0.3.0
With the new changes I could improve the speed drastically:
Creation time: * 1/2
Processing time: * 1/15
I however omitted the multiprocessing capabilities.
Speed increase was minimal and the new structure allowing single processor to be that fast
does not yet support MP usage.
The API did change!
- No separate factory anymore: Just create a CellularAutomaton(...)
- No Rule class anymore: Subclass CellularAutomaton and override `evolve_rule` and `init_cell_state`
- Cell color is now defined by the CAWindow `state_to_color_cb` parameter.
- Neighborhood does not need to know the dimension anymore
## Installation ## Installation
This module can be loaded and installed from [pipy](https://pypi.org/project/cellular-automaton/): `pip install cellular-automaton` This module can be loaded and installed from [pipy](https://pypi.org/project/cellular-automaton/): `pip install cellular-automaton`
## Usage ## Usage
To start and use the automaton you will have to define three things: To start and use the automaton you will have to define four things:
- The neighborhood - The neighborhood
- The dimensions of the grid - The dimensions of the grid
- The evolution rule - The evolution rule
- The initial cell state
`````python `````python
class MyCellularAutomaton(CellularAutomaton):
def init_cell_state(self, coordinate: tuple) -> Sequence:
return initial_cell_state
def evolve_rule(self, last_state: tuple, neighbors_last_states: Sequence) -> Sequence:
return next_cell_state
neighborhood = MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS) neighborhood = MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS)
ca = CAFactory.make_single_process_cellular_automaton(dimension=[100, 100], ca = MyCellularAutomaton(dimension=[100, 100],
neighborhood=neighborhood, neighborhood=neighborhood)
rule=MyRule)
`````` ``````
### Neighbourhood ### Neighbourhood
@ -59,39 +82,30 @@ The example above defines a two dimensional grid with 100 x 100 cells.
There is no limitation in how many dimensions you choose but your memory and processor power. There is no limitation in how many dimensions you choose but your memory and processor power.
### Rule ### Evolution and Initial State
The Rule has three tasks: To define the evolution rule and the initial state create a class inheriting from `CellularAutomaton`.
- Set the initial value for all cells. - The `init_cell_state` method will be called once during the creation process for every cell.
- Evolve a cell in respect to its neighbours. It will get the coordinate of that cell and is supposed to return a tuple representing that cells state.
- (optional) define how the cell should be drawn. - The `evolve_rule` gets passed the last cell state and the states of all neighbors.
It is supposed to return a tuple representing the new cell state.
`````python All new states will be applied simultaneously, so the order of processing the cells is irrelevant.
class MyRule(Rule):
def init_state(self, cell_coordinate):
return (1, 1)
def evolve_cell(self, last_cell_state, neighbors_last_states):
return self._get_neighbor_by_relative_coordinate(neighbors_last_states, (-1, -1))
def get_state_draw_color(self, current_state):
return [255 if current_state[0] else 0, 0, 0]
`````
Just inherit from `cellular_automaton.rule:Rule` and define the evolution rule and initial state.
## Visualisation ## Visualisation
The package provides a module for visualization in a pygame window for common two dimensional automatons. The package provides a module for visualization of a 2D automaton in a pygame window.
```
CAWindow(cellular_automaton=StarFallAutomaton()).run()
```
To add another kind of display option e.g. for other dimensions or hexagonal grids you can extrend the provided implementation or build your own.
The visual part of this module is fully decoupled and thus should be easily replaceable. The visual part of this module is fully decoupled and thus should be easily replaceable.
## Examples ## Examples
The package contains two examples: The package contains three examples:
- [simple_star_fall](https://gitlab.com/DamKoVosh/cellular_automaton/-/tree/master/examples/simple_star_fall.py) - [simple_star_fall](https://gitlab.com/DamKoVosh/cellular_automaton/-/tree/master/examples/simple_star_fall.py)
- [conways_game_of_life](https://gitlab.com/DamKoVosh/cellular_automaton/-/tree/master/examples/conways_game_of_life.py) - [conways_game_of_life](https://gitlab.com/DamKoVosh/cellular_automaton/-/tree/master/examples/conways_game_of_life.py)
- [creation_and_process_time_analysis](https://gitlab.com/DamKoVosh/cellular_automaton/-/tree/master/examples/times.py)
Those example automaton implementations should provide a good start for your own project. Those example implementations should provide a good start for your own project.
## Getting Involved ## Getting Involved
Feel free to open pull requests, send me feature requests or even join as developer. Feel free to open pull requests, send me feature requests or even join as developer.
@ -100,7 +114,7 @@ There ist still quite some work to do.
And for all others, don't hesitate to open issues when you have problems! And for all others, don't hesitate to open issues when you have problems!
## Dependencies ## Dependencies
For direct usage of the cellular automaton ther is no dependency. For direct usage of the cellular automaton there is no dependency.
If you want to use the display option however or execute the examples you will have to install If you want to use the display option however or execute the examples you will have to install
[pygame](https://www.pygame.org/news) for visualisation. [pygame](https://www.pygame.org/news) for visualisation.
If you do for some reason not want to use this engine simply inherit from display.DrawEngine and overwrite the If you do for some reason not want to use this engine simply inherit from display.DrawEngine and overwrite the

View File

@ -1 +0,0 @@
from cellular_automaton.cellular_automaton import *

View File

@ -1,5 +1,21 @@
from .neighborhood import Neighborhood, MooreNeighborhood, VonNeumannNeighborhood, \ #!/usr/bin/env python3
EdgeRule, HexagonalNeighborhood, RadialNeighborhood """
from .rule import Rule Copyright 2019 Richard Feistenauer
from .factory import CAFactory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from .neighborhood import Neighborhood, MooreNeighborhood, RadialNeighborhood, VonNeumannNeighborhood, \
HexagonalNeighborhood, EdgeRule
from .automaton import CellularAutomaton
from .display import CAWindow from .display import CAWindow

View File

@ -14,91 +14,122 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import multiprocessing from typing import Sequence
from multiprocessing.sharedctypes import RawValue import abc
from ctypes import c_int import itertools
import recordclass
from cellular_automaton import Neighborhood
class CellularAutomatonProcessor: CELL = recordclass.make_dataclass("Cell",
""" This class is responsible for the evolution of the cells. """ ("state", "is_active", "is_dirty", "neighbors"),
defaults=((0, ), True, True, (None, )))
def __init__(self, cellular_automaton):
self._ca = cellular_automaton
def evolve_x_times(self, x): class CellularAutomatonCreator(abc.ABC):
""" Evolve all cells x times. """ Creates a cellular automaton from a dimension and a neighborhood definition """
:param x: The number of evolution steps processed with the call of this method.
"""
for x in range(x):
self.evolve()
def evolve(self): def __init__(self,
""" Evolve all cells """ dimension,
self._ca.current_evolution_step += 1 neighborhood: Neighborhood,
i = self._ca.current_evolution_step *args, **kwargs):
r = self._ca.evolution_rule.evolve_cell super().__init__(*args, **kwargs)
list(map(lambda c: c.evolve_if_ready(r, i), tuple(self._ca.cells.values()))) self._dimension = dimension
self._neighborhood = neighborhood
self._current_state = {}
self._next_state = {}
self.__make_cellular_automaton_state()
def get_dimension(self): def get_dimension(self):
return self._ca.dimension return self._dimension
dimension = property(get_dimension)
def __make_cellular_automaton_state(self):
self.__make_cells()
self.__add_neighbors()
def __make_cells(self):
for coord in itertools.product(*[range(d) for d in self._dimension]):
self._current_state[coord] = CELL(self.init_cell_state(coord))
self._next_state[coord] = CELL(self.init_cell_state(coord))
def __add_neighbors(self):
calculate_neighbor_coordinates = self._neighborhood.calculate_cell_neighbor_coordinates
coordinates = self._current_state.keys()
for coordinate, cell_c, cell_n in zip(coordinates, self._current_state.values(), self._next_state.values()):
n_coord = calculate_neighbor_coordinates(coordinate, self._dimension)
cell_c.neighbors = list([self._current_state[nc] for nc in n_coord])
cell_n.neighbors = list([self._next_state[nc] for nc in n_coord])
def init_cell_state(self, cell_coordinate: Sequence) -> Sequence: # pragma: no cover
""" Will be called to initialize a cells state.
:param cell_coordinate: Cells coordinate.
:return: Iterable that represents the initial cell state
"""
raise NotImplementedError
class CellularAutomaton(CellularAutomatonCreator, abc.ABC):
"""
This class represents a cellular automaton.
It can be created with n dimensions and can handle different neighborhood definitions.
:param dimension: Iterable of len = dimensions
(e.g. [4, 3, 3, 3] = 4 x 3 x 3 x 3 cells in a four dimensional cube).
:param neighborhood: Defines which cells are considered neighbors.
"""
def __init__(self, neighborhood: Neighborhood, *args, **kwargs):
super().__init__(neighborhood=neighborhood, *args, **kwargs)
self._evolution_step = 0
def get_cells(self): def get_cells(self):
return self._ca.cells return self._current_state
def get_current_evolution_step(self): cells = property(get_cells)
return self._ca.current_evolution_step
def get_current_rule(self): def get_evolution_step(self):
return self._ca.evolution_rule return self._evolution_step
evolution_step = property(get_evolution_step)
class CellularAutomatonMultiProcessor(CellularAutomatonProcessor): def evolve(self, times=1):
""" This is a variant of CellularAutomatonProcessor that uses multi processing. """ Evolve all cells x times.
The evolution of the cells will be outsourced to new processes. :param times: The number of evolution steps processed with one call of this method.
WARNING:
This variant has high memory use!
The inter process communication overhead can make this variant slower than single processing!
""" """
for _ in itertools.repeat(None, times):
self.__evolve_cells(self._current_state, self._next_state)
self._current_state, self._next_state = self._next_state, self._current_state
self._evolution_step += 1
def __init__(self, cellular_automaton, process_count: int = 2): def __evolve_cells(self, this_state, next_state):
multiprocessing.freeze_support() evolve_cell = self.__evolve_cell
if process_count < 1: evolution_rule = self.evolve_rule
raise ValueError for old, new in zip(this_state.values(), next_state.values()):
if old.is_active:
new_state = evolution_rule(old.state, [n.state for n in old.neighbors])
old.is_active = False
evolve_cell(old, new, new_state)
super().__init__(cellular_automaton) @classmethod
def __evolve_cell(cls, old, cell, new_state):
changed = new_state != cell.state
cell.state = new_state
cell.is_dirty |= changed
old.is_dirty |= changed
if changed:
cell.is_active = True
for n in cell.neighbors:
n.is_active = True
self.evolve_range = range(len(self._ca.cells)) def evolve_rule(self, last_cell_state: Sequence, neighbors_last_states: Sequence) -> Sequence: # pragma: no cover
self._ca.current_evolution_step = RawValue(c_int, self._ca.current_evolution_step) """ Calculates and sets new state of 'cell'.
self.__init_processes(process_count) A cells evolution will only be called if it or at least one of its neighbors has changed last evolution_step.
:param last_cell_state: The cells state previous to the evolution step.
def __init_processes(self, process_count): :param neighbors_last_states: The cells neighbors current states.
self.pool = multiprocessing.Pool(processes=process_count, :return: New state. The state after this evolution step
initializer=_init_process, """
initargs=(tuple(self._ca.cells.values()), raise NotImplementedError
self._ca.evolution_rule,
self._ca.current_evolution_step))
def evolve(self):
self._ca.current_evolution_step.value += 1
self.pool.map(_process_routine, self.evolve_range)
def get_current_evolution_step(self):
return self._ca.current_evolution_step.value
global_cells = None
global_rule = None
global_evolution_step = None
def _init_process(cells, rule, index):
global global_rule, global_cells, global_evolution_step
global_cells = cells
global_rule = rule
global_evolution_step = index
def _process_routine(i):
global_cells[i].evolve_if_ready(global_rule.evolve_cell, global_evolution_step.value)

View File

@ -1,54 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from . import cell_state
class Cell:
def __init__(self, state_class: cell_state.CellState, neighbors):
self._state = state_class
self._neighbor_states = neighbors
def is_set_for_redraw(self):
""" Flag indicating a change in the cells state since last call of 'was_redrawn'. """
return self._state.is_set_for_redraw()
def was_redrawn(self):
""" Should be called after this cell was drawn to prevent unnecessary redraws. """
self._state.was_redrawn()
def get_current_state(self, evolution_step):
return self._state.get_state_of_evolution_step(evolution_step)
def evolve_if_ready(self, rule, evolution_step):
""" When there was a change in this cell or one of its neighbours,
evolution rule is called and the new state and redraw flag gets set if necessary.
If there was a change neighbours will be notified.
"""
if self._state.is_active(evolution_step):
new_state = rule(list(self._state.get_state_of_last_evolution_step(evolution_step)),
[list(n.get_state_of_last_evolution_step(evolution_step)) for n in self._neighbor_states])
self.__set_new_state_and_consider_activation(new_state, evolution_step)
def __set_new_state_and_consider_activation(self, new_state: cell_state.CellState, evolution_step):
changed = self._state.set_state_of_evolution_step(new_state, evolution_step)
self.__activate_if_necessary(changed, evolution_step)
def __activate_if_necessary(self, changed, evolution_step):
if changed:
self._state.set_active_for_next_evolution_step(evolution_step)
for n in self._neighbor_states:
n.set_active_for_next_evolution_step(evolution_step)

View File

@ -1,136 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from multiprocessing.sharedctypes import RawArray, RawValue
from ctypes import c_float, c_bool
class CellState:
"""
This is the base class for all cell states.
When using the cellular automaton display, inherit this class and implement get_state_draw_color.
"""
_state_save_slot_count = 2
def __init__(self, initial_state=(0., ), draw_first_state=True):
self._state_slots = [list(initial_state) for i in range(self.__class__._state_save_slot_count)]
self._active = [False for i in range(self.__class__._state_save_slot_count)]
self._active[0] = True
self._dirty = draw_first_state
def is_active(self, current_evolution_step):
""" Returns the active status for the requested evolution_step
:param current_evolution_step: The evolution_step of interest.
:return: True if the cell or one of its neighbours changed in the last evolution step.
"""
return self._active[self._calculate_slot(current_evolution_step)]
def set_active_for_next_evolution_step(self, current_evolution_step):
""" Sets the cell active for the next evolution_step, so it will be evolved.
:param current_evolution_step: The current evolution_step index.
"""
self._active[self._calculate_slot(current_evolution_step + 1)] = True
def is_set_for_redraw(self):
""" States if this state should be redrawn.
:return: True if state changed since last call of 'was_redrawn'.
"""
return self._dirty
def was_redrawn(self):
""" Remove the state from redraw cycle until next state change """
self._dirty = False
def get_state_of_last_evolution_step(self, current_evolution_step):
return self.get_state_of_evolution_step(current_evolution_step - 1)
def get_state_of_evolution_step(self, evolution_step):
""" Returns the state of the evolution_step.
:param evolution_step: Uses the evolution_step index, to differ between concurrent states.
:return The state of the requested evolution_step.
"""
return self._state_slots[self._calculate_slot(evolution_step)]
def set_state_of_evolution_step(self, new_state, evolution_step):
""" Sets the new state for the evolution_step.
:param new_state: The new state to set.
:param evolution_step: The evolution_step index, to differ between concurrent states.
:return True if the state really changed.
:raises IndexError: If the state length changed.
"""
changed = self._set_new_state_if_valid(new_state, evolution_step)
self._dirty |= changed
self._active[self._calculate_slot(evolution_step)] = False
return changed
def _set_new_state_if_valid(self, new_state, evolution_step):
current_state = self.get_state_of_evolution_step(evolution_step)
if len(new_state) != len(current_state):
raise IndexError("State length may not change!")
self.__change_current_state_values(current_state, new_state)
return self.__did_state_change(evolution_step)
@staticmethod
def __change_current_state_values(current_state, new_state):
for i, ns in enumerate(new_state):
if current_state[i] != ns:
current_state[i] = ns
def __did_state_change(self, evolution_step):
for a, b in zip(self.get_state_of_evolution_step(evolution_step),
self.get_state_of_last_evolution_step(evolution_step)):
if a != b:
return True
return False
def get_state_draw_color(self, evolution_step):
""" When implemented should return the color representing the requested state.
:param evolution_step: Requested evolution_step.
:return: Color of the state as rgb tuple
"""
raise NotImplementedError
@classmethod
def _calculate_slot(cls, evolution_step):
return evolution_step % cls._state_save_slot_count
class SynchronousCellState(CellState):
""" CellState version using shared values for multi processing purpose. """
def __init__(self, initial_state=(0., ), draw_first_state=True):
super().__init__(initial_state, draw_first_state)
self._state_slots = [RawArray(c_float, initial_state) for i in range(self.__class__._state_save_slot_count)]
self._active = [RawValue(c_bool, False) for i in range(self.__class__._state_save_slot_count)]
self._active[0].value = True
self._dirty = RawValue(c_bool, draw_first_state)
def set_active_for_next_evolution_step(self, current_evolution_step):
self._active[self._calculate_slot(current_evolution_step + 1)].value = True
def is_set_for_redraw(self):
return self._dirty.value
def was_redrawn(self):
self._dirty.value = False
def set_state_of_evolution_step(self, new_state, evolution_step):
changed = self._set_new_state_if_valid(new_state, evolution_step)
self._dirty.value |= changed
self._active[self._calculate_slot(evolution_step)].value = False
return changed

View File

@ -14,52 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
# pylint: disable=all
import time import time
import operator import operator
import collections
from typing import Sequence
from . import automaton from . import CellularAutomaton
_Rect = collections.namedtuple(typename="Rect",
field_names=["left", "top", "width", "height"])
class _Rect: class PygameEngine:
def __init__(self, left=0, top=0, width=0, height=0, rect=None, pos=None, size=None): """ This is an wrapper for the pygame engine.
if rect is not None and (pos is not None or size is not None): By initializing pygame lazy the dependency can be dropped.
raise ValueError("define either rect OR position and size OR left, top, width and height") """
if not (left == top == width == height == 0) and not(rect == pos == size is None):
raise ValueError("define either rect OR position and size OR left, top, width and height")
self.__direct_initialisation(height, left, top, width) def __init__(self, window_size, *args, **kwargs):
self.__pos_and_size_initialisation(pos, size)
self.__rect_initialisation(rect)
def __rect_initialisation(self, rect):
if rect is not None:
self.__direct_initialisation(rect[1][1], rect[0][0], rect[0][1], rect[1][0])
def __pos_and_size_initialisation(self, pos, size):
if pos is not None:
self.left = pos[0]
self.top = pos[1]
if size is not None:
self.width = size[0]
self.height = size[1]
def __direct_initialisation(self, height, left, top, width):
self.left = left
self.top = top
self.width = width
self.height = height
def get_rect_tuple(self):
return (self.left, self.top), (self.width, self.height)
class DrawEngine(object):
def __init__(self, window_size=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
global pygame
import pygame import pygame
pygame.init() self._pygame = pygame
self._pygame.init()
pygame.display.set_caption("Cellular Automaton") pygame.display.set_caption("Cellular Automaton")
self.__screen = pygame.display.set_mode(window_size) self.__screen = pygame.display.set_mode(window_size)
self.__font = pygame.font.SysFont("monospace", 15) self.__font = pygame.font.SysFont("monospace", 15)
@ -70,93 +47,110 @@ class DrawEngine(object):
def write_text(self, pos, text, color=(0, 255, 0)): def write_text(self, pos, text, color=(0, 255, 0)):
label = self.__font.render(text, 1, color) label = self.__font.render(text, 1, color)
update_rect = self.__screen.blit(label, pos) update_rect = self.__screen.blit(label, pos)
DrawEngine.update_rectangles(update_rect) self.update_rectangles(update_rect)
def fill_surface_with_color(self, rect, color=(0, 0, 0)): def fill_surface_with_color(self, rect, color=(0, 0, 0)):
return self.__screen.fill(color, rect) return self.__screen.fill(color, rect)
@staticmethod def update_rectangles(self, rectangles):
def update_rectangles(rectangles): self._pygame.display.update(rectangles)
pygame.display.update(rectangles)
@staticmethod def is_active(self): # pragma: no cover
def is_active(): for event in self._pygame.event.get():
for event in pygame.event.get(): if event.type == self._pygame.QUIT:
if event.type == pygame.QUIT:
return False return False
return True return True
class _CASurface: class CAWindow:
def __init__(self, def __init__(self,
grid_rect, cellular_automaton: CellularAutomaton,
cellular_automaton: automaton.CellularAutomatonProcessor, window_size=(1000, 800),
draw_engine, draw_engine=None,
state_to_color_cb=None,
*args, **kwargs): *args, **kwargs):
"""
Creates a window to render a 2D CellularAutomaton.
:param cellular_automaton: The automaton to display and evolve
:param window_size: The Window size (default: 1000 x 800)
:param draw_engine: The draw_engine (default: pygame)
:param state_to_color_cb: A callback to define the draw color of CA states (default: red for states != 0)
"""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._cellular_automaton = cellular_automaton self._cellular_automaton = cellular_automaton
self.__rect = grid_rect self.__rect = _Rect(left=0, top=30, width=window_size[0], height=window_size[1] - 30)
self.__cell_size = self._calculate_cell_display_size() self.__calculate_cell_display_size()
self.__draw_engine = draw_engine self.__draw_engine = PygameEngine(window_size) if draw_engine is None else draw_engine
self.__state_to_color = self._get_cell_color if state_to_color_cb is None else state_to_color_cb
def _calculate_cell_display_size(self): def run(self,
grid_dimension = self._cellular_automaton.get_dimension() evolutions_per_second=0,
return [self.__rect.width / grid_dimension[0], self.__rect.height / grid_dimension[1]] evolutions_per_draw=1,
last_evolution_step=0,):
"""
Evolves and draws the CellularAutomaton
:param evolutions_per_second: 0 = as fast as possible | > 0 to slow down the CellularAutomaton
:param evolutions_per_draw: Amount of evolutions done before screen gets redrawn.
:param last_evolution_step: 0 = infinite | > 0 evolution step at which this method will stop
Warning: is blocking until finished
"""
while self._is_not_user_terminated() and self._not_at_the_end(last_evolution_step):
time_ca_start = time.time()
self._cellular_automaton.evolve(evolutions_per_draw)
time_ca_end = time.time()
self._redraw_dirty_cells()
time_ds_end = time.time()
self.print_process_info(evolve_duration=(time_ca_end - time_ca_start),
draw_duration=(time_ds_end - time_ca_end),
evolution_step=self._cellular_automaton.evolution_step)
self._sleep_to_keep_rate(time.time() - time_ca_start, evolutions_per_second)
def redraw_cellular_automaton(self): def _sleep_to_keep_rate(self, time_taken, evolutions_per_second): # pragma: no cover
""" Redraws those cells which changed their state since last redraw. """ if evolutions_per_second > 0:
rest_time = 1.0 / evolutions_per_second - time_taken
if rest_time > 0:
time.sleep(rest_time)
def _not_at_the_end(self, last_evolution_step):
return self._cellular_automaton.evolution_step < last_evolution_step or last_evolution_step <= 0
def __calculate_cell_display_size(self):
grid_dimension = self._cellular_automaton.dimension
self.__cell_size = [self.__rect.width / grid_dimension[0], self.__rect.height / grid_dimension[1]]
def _redraw_dirty_cells(self):
self.__draw_engine.update_rectangles(list(self.__redraw_dirty_cells())) self.__draw_engine.update_rectangles(list(self.__redraw_dirty_cells()))
def __redraw_dirty_cells(self): def __redraw_dirty_cells(self):
for coordinate, cell in self._cellular_automaton.get_cells().items(): for coordinate, cell in self._cellular_automaton.cells.items():
if cell.is_set_for_redraw(): if cell.is_dirty:
yield from self.__redraw_cell(cell, coordinate) yield self.__redraw_cell(cell, coordinate)
def __redraw_cell(self, cell, coordinate): def __redraw_cell(self, cell, coordinate):
cell_color = self.__get_cell_color(cell) cell_color = self.__state_to_color(cell.state)
cell_pos = self._calculate_cell_position_in_the_grid(coordinate) cell_pos = self.__calculate_cell_position_in_the_grid(coordinate)
surface_pos = self._calculate_cell_position_on_screen(cell_pos) surface_pos = self.__calculate_cell_position_on_screen(cell_pos)
cell.was_redrawn() cell.is_dirty = False
yield self._draw_cell_surface(surface_pos, cell_color) return self.__draw_cell_surface(surface_pos, cell_color)
def __get_cell_color(self, cell): def _get_cell_color(self, current_state: Sequence) -> Sequence:
return self._cellular_automaton.get_current_rule().get_state_draw_color( """ Returns the color of the cell depending on its current state """
cell.get_current_state(self._cellular_automaton.get_current_evolution_step())) return 255 if current_state[0] else 0, 0, 0
def _calculate_cell_position_in_the_grid(self, coordinate): def __calculate_cell_position_in_the_grid(self, coordinate):
return list(map(operator.mul, self.__cell_size, coordinate)) return list(map(operator.mul, self.__cell_size, coordinate))
def _calculate_cell_position_on_screen(self, cell_pos): def __calculate_cell_position_on_screen(self, cell_pos):
return [self.__rect.left + cell_pos[0], self.__rect.top + cell_pos[1]] return [self.__rect.left + cell_pos[0], self.__rect.top + cell_pos[1]]
def _draw_cell_surface(self, surface_pos, cell_color): def __draw_cell_surface(self, surface_pos, cell_color):
return self.__draw_engine.fill_surface_with_color((surface_pos, self.__cell_size), cell_color) return self.__draw_engine.fill_surface_with_color((surface_pos, self.__cell_size), cell_color)
def print_process_info(self, evolve_duration, draw_duration, evolution_step):
self.__draw_engine.fill_surface_with_color(((0, 0), (self.__rect.width, 30)))
self.__draw_engine.write_text((10, 5), "CA: " + "{0:.4f}".format(evolve_duration) + "s")
self.__draw_engine.write_text((310, 5), "Display: " + "{0:.4f}".format(draw_duration) + "s")
self.__draw_engine.write_text((660, 5), "Step: " + str(evolution_step))
class CAWindow(DrawEngine): def _is_not_user_terminated(self):
def __init__(self, cellular_automaton: automaton.CellularAutomatonProcessor, return self.__draw_engine.is_active()
evolution_steps_per_draw=1,
window_size=(1000, 800),
*args, **kwargs):
super().__init__(window_size=window_size, *args, **kwargs)
self._ca = cellular_automaton
self.ca_display = _CASurface(_Rect(pos=(0, 30), size=(window_size[0], window_size[1] - 30)),
self._ca,
self)
self.__loop_evolution_and_redraw_of_automaton(evolution_steps_per_draw=evolution_steps_per_draw)
def __loop_evolution_and_redraw_of_automaton(self, evolution_steps_per_draw):
while super().is_active():
time_ca_start = time.time()
self._ca.evolve_x_times(evolution_steps_per_draw)
time_ca_end = time.time()
self.ca_display.redraw_cellular_automaton()
time_ds_end = time.time()
self.__print_process_duration(time_ca_end, time_ca_start, time_ds_end)
def __print_process_duration(self, time_ca_end, time_ca_start, time_ds_end):
super().fill_surface_with_color(_Rect(size=(self._width, 30)).get_rect_tuple())
super().write_text((10, 5), "CA: " + "{0:.4f}".format(time_ca_end - time_ca_start) + "s")
super().write_text((310, 5), "Display: " + "{0:.4f}".format(time_ds_end - time_ca_end) + "s")
super().write_text((660, 5), "Step: " + str(self._ca.get_current_evolution_step()))

View File

@ -1,76 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import itertools
from typing import Type
from . import Neighborhood, Rule
from .automaton import CellularAutomatonProcessor, CellularAutomatonMultiProcessor
from .cell import Cell
from .state import CellularAutomatonState
from .cell_state import CellState, SynchronousCellState
class CAFactory:
""" This factory provides an easy way to create cellular automatons with single or multi processing. """
@staticmethod
def make_single_process_cellular_automaton(dimension,
neighborhood: Neighborhood,
rule: Type[Rule]):
ca = CAFactory._make_cellular_automaton_state(dimension, neighborhood, CellState, rule)
return CellularAutomatonProcessor(ca)
@staticmethod
def _make_cellular_automaton_state(dimension, neighborhood, state_class, rule_class):
rule = rule_class(neighborhood)
cell_states = CAFactory._make_cell_states(state_class, rule, dimension)
cells = CAFactory._make_cells(cell_states, neighborhood, dimension)
return CellularAutomatonState(cells, dimension, rule)
@staticmethod
def make_multi_process_cellular_automaton(dimension,
neighborhood: Neighborhood,
rule: Type[Rule],
processes: int):
if processes < 1:
raise ValueError("At least one process is necessary")
elif processes == 1:
return CAFactory.make_single_process_cellular_automaton(dimension, neighborhood, rule)
else:
ca = CAFactory._make_cellular_automaton_state(dimension, neighborhood, SynchronousCellState, rule)
return CellularAutomatonMultiProcessor(ca, processes)
@staticmethod
def _make_cell_states(state_class, rule, dimension):
cell_states = {}
for c in itertools.product(*[range(d) for d in dimension]):
coordinate = tuple(c)
cell_states[coordinate] = state_class(rule.init_state(coordinate))
return cell_states
@staticmethod
def _make_cells(cell_states, neighborhood, dimension):
cells = {}
for coordinate, cell_state in cell_states.items():
n_coordinates = neighborhood.calculate_cell_neighbor_coordinates(coordinate, dimension)
neighbor_states = tuple([cell_states[tuple(nc)] for nc in n_coordinates])
cells[coordinate] = Cell(cell_state, neighbor_states)
return cells

View File

@ -21,20 +21,25 @@ import math
class EdgeRule(enum.Enum): class EdgeRule(enum.Enum):
""" Enum for different possibilities to handle the edge of the automaton. """
IGNORE_EDGE_CELLS = 0 IGNORE_EDGE_CELLS = 0
IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS = 1 IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS = 1
FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS = 2 FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS = 2
class Neighborhood: class Neighborhood:
def __init__(self, neighbors_relative, edge_rule: EdgeRule): """ Defines which cells should be considered to be neighbors during evolution of cellular automaton."""
""" Defines a neighborhood of a cell.
:param neighbors_relative: List of relative coordinates for cell neighbors. def __init__(self, edge_rule=EdgeRule.IGNORE_EDGE_CELLS, radius=1):
:param edge_rule: EdgeRule to define, how cells on the edge of the grid will be handled. """ General class for all Neighborhoods.
:param edge_rule: Rule to define, how cells on the edge of the grid will be handled.
:param radius: If radius > 1 it grows the neighborhood
by adding the neighbors of the neighbors radius times.
""" """
self._rel_neighbors = neighbors_relative self._rel_neighbors = None
self._grid_dimensions = []
self._radius = radius
self.__edge_rule = edge_rule self.__edge_rule = edge_rule
self.__grid_dimensions = []
def calculate_cell_neighbor_coordinates(self, cell_coordinate, grid_dimensions): def calculate_cell_neighbor_coordinates(self, cell_coordinate, grid_dimensions):
""" Get a list of absolute coordinates for the cell neighbors. """ Get a list of absolute coordinates for the cell neighbors.
@ -43,31 +48,42 @@ class Neighborhood:
:param grid_dimensions: The dimensions of the grid, to apply the edge the rule. :param grid_dimensions: The dimensions of the grid, to apply the edge the rule.
:return: list of absolute coordinates for the cells neighbors. :return: list of absolute coordinates for the cells neighbors.
""" """
self.__grid_dimensions = grid_dimensions self.__lazy_initialize_relative_neighborhood(grid_dimensions)
return list(self._neighbors_generator(cell_coordinate)) return tuple(self._neighbors_generator(cell_coordinate))
def get_id_of_neighbor_from_relative_coordinate(self, rel_coordinate): def __lazy_initialize_relative_neighborhood(self, grid_dimensions):
return self._rel_neighbors.index(rel_coordinate) self._grid_dimensions = grid_dimensions
if self._rel_neighbors is None:
self._create_relative_neighborhood()
def _create_relative_neighborhood(self):
self._rel_neighbors = tuple(self._neighborhood_generator())
def _neighborhood_generator(self):
for coordinate in itertools.product(range(-self._radius, self._radius + 1), repeat=len(self._grid_dimensions)):
if self._neighbor_rule(coordinate) and coordinate != (0, ) * len(self._grid_dimensions):
yield tuple(reversed(coordinate))
def _neighbor_rule(self, rel_neighbor): # pylint: disable=no-self-use, unused-argument
return True
def get_neighbor_by_relative_coordinate(self, neighbors, rel_coordinate):
return neighbors[self._rel_neighbors.index(rel_coordinate)]
def _neighbors_generator(self, cell_coordinate): def _neighbors_generator(self, cell_coordinate):
if not self._does_ignore_edge_cell_rule_apply(cell_coordinate): on_edge = self.__is_coordinate_on_an_edge(cell_coordinate)
if self.__edge_rule != EdgeRule.IGNORE_EDGE_CELLS or not on_edge: # pylint: disable=too-many-nested-blocks
for rel_n in self._rel_neighbors: for rel_n in self._rel_neighbors:
yield from self._calculate_abs_neighbor_and_decide_validity(cell_coordinate, rel_n) if on_edge:
n, n_folded = zip(*[(ni + ci, (ni + di + ci) % di)
def _calculate_abs_neighbor_and_decide_validity(self, cell_coordinate, rel_n): for ci, ni, di in zip(cell_coordinate, rel_n, self._grid_dimensions)])
n = list(map(operator.add, rel_n, cell_coordinate)) if self.__edge_rule == EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS or n == n_folded:
n_folded = self.__apply_edge_overflow(n)
if n == n_folded or self.__edge_rule == EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS:
yield n_folded yield n_folded
else:
def _does_ignore_edge_cell_rule_apply(self, coordinate): yield tuple(map(operator.add, rel_n, cell_coordinate))
return self.__edge_rule == EdgeRule.IGNORE_EDGE_CELLS and self.__is_coordinate_on_an_edge(coordinate)
def __is_coordinate_on_an_edge(self, coordinate): def __is_coordinate_on_an_edge(self, coordinate):
return any(0 == ci or ci == di-1 for ci, di in zip(coordinate, self.__grid_dimensions)) return any(ci in [0, di-1] for ci, di in zip(coordinate, self._grid_dimensions))
def __apply_edge_overflow(self, n):
return list(map(lambda ni, di: (ni + di) % di, n, self.__grid_dimensions))
class MooreNeighborhood(Neighborhood): class MooreNeighborhood(Neighborhood):
@ -86,10 +102,6 @@ class MooreNeighborhood(Neighborhood):
X X X X X N N N N N X X X X X N N N N N
""" """
def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, radius=1, dimension=2):
super().__init__(tuple(_rel_neighbor_generator(dimension, radius, lambda rel_n: True)),
edge_rule)
class VonNeumannNeighborhood(Neighborhood): class VonNeumannNeighborhood(Neighborhood):
""" Von Neumann defined a neighborhood with a radius applied to Manhatten distance """ Von Neumann defined a neighborhood with a radius applied to Manhatten distance
@ -108,16 +120,11 @@ class VonNeumannNeighborhood(Neighborhood):
X X X X X X X N X X X X X X X X X N X X
""" """
def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, radius=1, dimension=2): def _neighbor_rule(self, rel_neighbor):
self.radius = radius
super().__init__(tuple(_rel_neighbor_generator(dimension, radius, self.neighbor_rule)),
edge_rule)
def neighbor_rule(self, rel_n):
cross_sum = 0 cross_sum = 0
for ci in rel_n: for coordinate_i in rel_neighbor:
cross_sum += abs(ci) cross_sum += abs(coordinate_i)
return cross_sum <= self.radius return cross_sum <= self._radius
class RadialNeighborhood(Neighborhood): class RadialNeighborhood(Neighborhood):
@ -139,18 +146,16 @@ class RadialNeighborhood(Neighborhood):
X X X X X X X X X N N N X X X X X X X X X X X N N N X X
""" """
def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, radius=1, delta_=.25, dimension=2): def __init__(self, *args, delta_=.25, **kwargs):
self.radius = radius
self.delta = delta_ self.delta = delta_
super().__init__(tuple(_rel_neighbor_generator(dimension, radius, self.neighbor_rule)), super().__init__(*args, **kwargs)
edge_rule)
def neighbor_rule(self, rel_n): def _neighbor_rule(self, rel_neighbor):
cross_sum = 0 cross_sum = 0
for ci in rel_n: for coordinate_i in rel_neighbor:
cross_sum += pow(ci, 2) cross_sum += pow(coordinate_i, 2)
return math.sqrt(cross_sum) <= self.radius + self.delta return math.sqrt(cross_sum) <= self._radius + self.delta
class HexagonalNeighborhood(Neighborhood): class HexagonalNeighborhood(Neighborhood):
@ -186,30 +191,27 @@ class HexagonalNeighborhood(Neighborhood):
X N N N X X N N N X X N N N X X N N N X
""" """
def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, radius=1): def __init__(self, *args, radius=1, **kwargs):
neighbor_lists = [[(0, 0)], super().__init__(radius=radius, *args, **kwargs)
[(0, 0)]] self.__calculate_hexagonal_neighborhood(radius)
self.__calculate_hexagonal_neighborhood(neighbor_lists, radius) def __calculate_hexagonal_neighborhood(self, radius):
neighbor_lists = [[(0, 0)], [(0, 0)]]
for radius_i in range(1, radius + 1):
for i, neighbor in enumerate(neighbor_lists):
neighbor = _grow_neighbours(neighbor)
neighbor = self.__add_rectangular_neighbours(neighbor, radius_i, i % 2 == 1)
neighbor = sorted(neighbor, key=(lambda ne: [ne[1], ne[0]]))
neighbor.remove((0, 0))
neighbor_lists[i] = neighbor
self._neighbor_lists = neighbor_lists
super().__init__(neighbor_lists, edge_rule) def get_neighbor_by_relative_coordinate(self, neighbors, rel_coordinate): # pragma: no cover
def __calculate_hexagonal_neighborhood(self, neighbor_lists, radius):
for r in range(1, radius + 1):
for i, n in enumerate(neighbor_lists):
n = _grow_neighbours(n)
n = self.__add_rectangular_neighbours(n, r, i % 2 == 1)
n = sorted(n, key=(lambda ne: [ne[1], ne[0]]))
n.remove((0, 0))
neighbor_lists[i] = n
def get_id_of_neighbor_from_relative_coordinate(self, rel_coordinate):
raise NotImplementedError raise NotImplementedError
def _neighbors_generator(self, cell_coordinate): def calculate_cell_neighbor_coordinates(self, cell_coordinate, grid_dimensions):
if not self._does_ignore_edge_cell_rule_apply(cell_coordinate): self._rel_neighbors = self._neighbor_lists[cell_coordinate[1] % 2]
for rel_n in self._rel_neighbors[cell_coordinate[1] % 2]: return super().calculate_cell_neighbor_coordinates(cell_coordinate, grid_dimensions)
yield from self._calculate_abs_neighbor_and_decide_validity(cell_coordinate, rel_n)
@staticmethod @staticmethod
def __add_rectangular_neighbours(neighbours, radius, is_odd): def __add_rectangular_neighbours(neighbours, radius, is_odd):
@ -226,12 +228,6 @@ class HexagonalNeighborhood(Neighborhood):
return list(set(new_neighbours)) return list(set(new_neighbours))
def _rel_neighbor_generator(dimension, range_, rule):
for c in itertools.product(range(-range_, range_ + 1), repeat=dimension):
if rule(c) and c != (0, ) * dimension:
yield tuple(reversed(c))
def _grow_neighbours(neighbours): def _grow_neighbours(neighbours):
new_neighbours = neighbours[:] new_neighbours = neighbours[:]
for n in neighbours: for n in neighbours:

View File

@ -1,55 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import abc
from . import neighborhood
class Rule:
""" Base class for evolution rules.
This class has to be inherited if the cellular automaton is supposed to have any effect.
"""
def __init__(self, neighborhood_: neighborhood.Neighborhood):
self._neighborhood = neighborhood_
def _get_neighbor_by_relative_coordinate(self, neighbours, rel_coordinate):
return neighbours[self._neighborhood.get_id_of_neighbor_from_relative_coordinate(rel_coordinate)]
@abc.abstractmethod
def evolve_cell(self, last_cell_state, neighbors_last_states):
""" Calculates and sets new state of 'cell'.
A cells evolution will only be called if it or at least one of its neighbors has changed last evolution_step.
:param last_cell_state: The cells state previous to the evolution step.
:param neighbors_last_states: The cells neighbors current states.
:return: New state. The state after this evolution step
"""
return last_cell_state
@abc.abstractmethod
def init_state(self, cell_coordinate):
""" Will be called to initialize a cells state.
:param cell_coordinate: Cells coordinate.
:return: Iterable that represents the initial cell state
Has to be compatible with 'multiprocessing.sharedctype.RawArray' when using multi processing.
"""
return [0]
@abc.abstractmethod
def get_state_draw_color(self, current_state):
""" Return the draw color for the current state """
return [0, 0, 0]

View File

@ -1,27 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from . import Rule
class CellularAutomatonState:
""" Holds all relevant information about the cellular automaton """
def __init__(self, cells, dimension, evolution_rule: Rule):
self.cells = cells
self.dimension = dimension
self.evolution_rule = evolution_rule
self.current_evolution_step = 0

View File

@ -15,23 +15,34 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import random # pylint: disable=wrong-import-position
from cellular_automaton import * # pylint: disable=missing-function-docstring
import random
import contextlib
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from cellular_automaton import CellularAutomaton, MooreNeighborhood, CAWindow, EdgeRule
ALIVE = [1.0] ALIVE = [1.0]
DEAD = [0] DEAD = [0]
class ConwaysRule(Rule): class ConwaysCA(CellularAutomaton):
random_seed = random.seed(13) """ Cellular automaton with the evolution rules of conways game of life """
def init_state(self, cell_coordinate): def __init__(self):
super().__init__(dimension=[100, 100],
neighborhood=MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS))
def init_cell_state(self, __): # pylint: disable=no-self-use
rand = random.randrange(0, 16, 1) rand = random.randrange(0, 16, 1)
init = max(.0, float(rand - 14)) init = max(.0, float(rand - 14))
return [init] return [init]
def evolve_cell(self, last_cell_state, neighbors_last_states): def evolve_rule(self, last_cell_state, neighbors_last_states):
new_cell_state = last_cell_state new_cell_state = last_cell_state
alive_neighbours = self.__count_alive_neighbours(neighbors_last_states) alive_neighbours = self.__count_alive_neighbours(neighbors_last_states)
if last_cell_state == DEAD and alive_neighbours == 3: if last_cell_state == DEAD and alive_neighbours == 3:
@ -46,20 +57,13 @@ class ConwaysRule(Rule):
@staticmethod @staticmethod
def __count_alive_neighbours(neighbours): def __count_alive_neighbours(neighbours):
an = [] alive_neighbors = []
for n in neighbours: for n in neighbours:
if n == ALIVE: if n == ALIVE:
an.append(1) alive_neighbors.append(1)
return len(an) return len(alive_neighbors)
def get_state_draw_color(self, current_state):
return [255 if current_state[0] else 0, 0, 0]
if __name__ == "__main__": if __name__ == "__main__":
neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS) with contextlib.suppress(KeyboardInterrupt):
ca = CAFactory.make_multi_process_cellular_automaton(dimension=[100, 100], CAWindow(cellular_automaton=ConwaysCA()).run(evolutions_per_second=40)
neighborhood=neighborhood,
rule=ConwaysRule,
processes=4)
ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1)

View File

@ -15,29 +15,43 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
# pylint: disable=wrong-import-position
# pylint: disable=missing-function-docstring
# pylint: disable=no-self-use
import random import random
from cellular_automaton import * import contextlib
import sys
import os
from typing import Sequence
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from cellular_automaton import CellularAutomaton, MooreNeighborhood, CAWindow, EdgeRule
class StarfallRule(Rule): class StarFallAutomaton(CellularAutomaton):
""" A basic cellular automaton that just copies one neighbour state so get some motion in the grid. """ """ Represents an automaton dropping colorful stars """
random_seed = random.seed(1000)
def init_state(self, cell_coordinate): def __init__(self):
super().__init__(dimension=[100, 100],
neighborhood=MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS))
def init_cell_state(self, __) -> Sequence:
rand = random.randrange(0, 101, 1) rand = random.randrange(0, 101, 1)
init = max(.0, float(rand - 99)) init = max(.0, float(rand - 99))
return [init] return [init * random.randint(0, 3)]
def evolve_cell(self, last_cell_state, neighbors_last_states): def evolve_rule(self, __, neighbors_last_states: Sequence) -> Sequence:
return self._get_neighbor_by_relative_coordinate(neighbors_last_states, (-1, -1)) return self._neighborhood.get_neighbor_by_relative_coordinate(neighbors_last_states, (-1, -1))
def get_state_draw_color(self, current_state):
return [255 if current_state[0] else 0, 0, 0] def state_to_color(current_state: Sequence) -> Sequence:
return 255 if current_state[0] == 1 else 0, \
255 if current_state[0] == 2 else 0, \
255 if current_state[0] == 3 else 0
if __name__ == "__main__": if __name__ == "__main__":
neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS) with contextlib.suppress(KeyboardInterrupt):
ca = CAFactory.make_single_process_cellular_automaton(dimension=[100, 100], CAWindow(cellular_automaton=StarFallAutomaton(), state_to_color_cb=state_to_color).run()
neighborhood=neighborhood,
rule=StarfallRule)
ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1)

63
examples/times.py Normal file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# pylint: disable=wrong-import-position
# pylint: disable=missing-function-docstring
# pylint: disable=no-self-use
import pstats
import random
import tempfile
import cProfile
import contextlib
import sys
import os
from typing import Sequence
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from cellular_automaton import CellularAutomaton, MooreNeighborhood, EdgeRule
class StarFallAutomaton(CellularAutomaton):
""" A basic cellular automaton that just copies one neighbour state so get some motion in the grid. """
def __init__(self):
super().__init__(dimension=[20, 20, 10, 10],
neighborhood=MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS))
def init_cell_state(self, __) -> Sequence:
rand = random.randrange(0, 101, 1)
init = max(.0, float(rand - 99))
return [init * random.randint(0, 3)]
def evolve_rule(self, __, neighbors_last_states: Sequence) -> Sequence:
return self._neighborhood.get_neighbor_by_relative_coordinate(neighbors_last_states, (-1, -1, -1, -1))
def profile(code):
with tempfile.NamedTemporaryFile() as temp_file:
cProfile.run(code, filename=temp_file.name, sort=True)
profile_stats = pstats.Stats(temp_file.name)
profile_stats.sort_stats("tottime").print_stats(20)
if __name__ == "__main__":
with contextlib.suppress(KeyboardInterrupt):
profile('ca = StarFallAutomaton()')
profile('ca.evolve(times=10)')

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[metadata]
description-file = README.md

View File

@ -1,19 +1,20 @@
from setuptools import setup # -*- coding: utf-8 -*-
from setuptools import setup, find_packages
with open('README.md') as f: with open('README.md') as f:
long_description = f.read() long_description = f.read()
setup( setup(
name="cellular_automaton", name="cellular_automaton",
version="0.1.0", version="1.0.0",
author="Richard Feistenauer", author="Richard Feistenauer",
author_email="r.feistenauer@web.de", author_email="r.feistenauer@web.de",
packages=["cellular_automaton"], packages=find_packages(exclude=('tests', 'docs', 'examples')),
url="https://gitlab.com/DamKoVosh/cellular_automaton", url="https://gitlab.com/DamKoVosh/cellular_automaton",
license="Apache License 2.0", license="Apache License 2.0",
description="N dimensional cellular automaton with multi processing capability.", description="N dimensional cellular automaton.",
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
requires=[""], requires=["Python (>3.6.1)", "recordclass", "pytest"]
python_requires='>3.6.1'
) )

View File

@ -1,72 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
sys.path.append('../cellular_automaton')
from cellular_automaton import *
import unittest
class TestRule(Rule):
def evolve_cell(self, last_cell_state, neighbors_last_states):
return [last_cell_state[0] + 1]
def init_state(self, cell_coordinate):
return [0]
def get_state_draw_color(self, current_state):
return [0, 0, 0]
class TestCellState(unittest.TestCase):
def setUp(self):
self.neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
self.processor = CAFactory.make_single_process_cellular_automaton([3, 3],
self.neighborhood,
TestRule)
def test_single_process_evolution_steps(self):
self.processor.evolve_x_times(5)
self.assertEqual(self.processor.get_current_evolution_step(), 5)
def test_multi_process_evolution_steps(self):
self.__create_multi_process_automaton()
self.multi_processor.evolve_x_times(5)
self.assertEqual(self.multi_processor.get_current_evolution_step(), 5)
def __create_multi_process_automaton(self):
self.multi_processor = CAFactory.make_multi_process_cellular_automaton([3, 3],
self.neighborhood,
TestRule,
processes=2)
def test_single_process_evolution_calls(self):
self.processor.evolve_x_times(5)
step = self.processor.get_current_evolution_step()
cell = self.processor.get_cells()[(1, 1)].get_current_state(step)[0]
self.assertEqual(cell, 4)
def test_multi_process_evolution_calls(self):
self.__create_multi_process_automaton()
self.multi_processor.evolve_x_times(5)
step = self.multi_processor.get_current_evolution_step()
cell = self.multi_processor.get_cells()[(1, 1)].get_current_state(step)[0]
self.assertEqual(cell, 4)
if __name__ == '__main__':
unittest.main()

View File

@ -1,52 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
sys.path.append('..')
from cellular_automaton.cellular_automaton.cell import Cell
from cellular_automaton.cellular_automaton.cell_state import CellState
import unittest
class TestCellState(unittest.TestCase):
def setUp(self):
self.neighbors = [CellState() for x in range(5)]
for neighbor in self.neighbors:
neighbor.set_state_of_evolution_step((0, ), 0)
self.cell = Cell(CellState(), self.neighbors)
def cell_and_neighbors_active(self, evolution_step):
self.neighbors.append(self.cell._state)
all_active = True
for state in self.neighbors:
if not state.is_active(evolution_step):
all_active = False
return all_active
def test_evolve_activation(self):
self.cell.evolve_if_ready((lambda a, b: (1,)), 0)
all_active = self.cell_and_neighbors_active(1)
self.assertTrue(all_active)
def test_evolve_activation_on_no_change(self):
self.cell.evolve_if_ready((lambda a, b: (0,)), 0)
all_active = self.cell_and_neighbors_active(1)
self.assertFalse(all_active)
if __name__ == '__main__':
unittest.main()

View File

@ -1,73 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
sys.path.append('../cellular_automaton')
from cellular_automaton.cellular_automaton.cell_state import SynchronousCellState
import unittest
class TestCellState(unittest.TestCase):
def setUp(self):
self.cell_state = SynchronousCellState(initial_state=(0,), draw_first_state=False)
def test_get_state_with_overflow(self):
self.cell_state.set_state_of_evolution_step(new_state=(1,), evolution_step=0)
self.assertEqual(tuple(self.cell_state.get_state_of_evolution_step(2)), (1,))
def test_set_state_with_overflow(self):
self.cell_state.set_state_of_evolution_step(new_state=(1,), evolution_step=3)
self.assertEqual(tuple(self.cell_state.get_state_of_evolution_step(1)), (1,))
def test_set_state_does_not_effect_all_slots(self):
self.cell_state.set_state_of_evolution_step(new_state=(1,), evolution_step=0)
self.assertEqual(tuple(self.cell_state.get_state_of_evolution_step(1)), (0,))
def test_redraw_state_on_change(self):
self.cell_state.set_state_of_evolution_step(new_state=(1,), evolution_step=0)
self.assertTrue(self.cell_state.is_set_for_redraw())
def test_redraw_state_on_nochange(self):
self.cell_state.set_state_of_evolution_step(new_state=(0,), evolution_step=0)
self.assertFalse(self.cell_state.is_set_for_redraw())
def test_active_state_after_set(self):
self.cell_state.set_state_of_evolution_step(new_state=(1,), evolution_step=0)
self.assertFalse(self.cell_state.is_active(0))
self.assertFalse(self.cell_state.is_active(1))
def test_set_active_for_next_evolution_step(self):
self.cell_state.set_state_of_evolution_step(new_state=(1,), evolution_step=0)
self.cell_state.set_active_for_next_evolution_step(0)
self.assertFalse(self.cell_state.is_active(0))
self.assertTrue(self.cell_state.is_active(1))
def test_new_state_length(self):
self.assertRaises(IndexError, self.__set_state_with_new_length)
def __set_state_with_new_length(self):
return self.cell_state.set_state_of_evolution_step(new_state=(1, 1), evolution_step=0)
def test_redraw_flag(self):
self.cell_state = SynchronousCellState(initial_state=(0,), draw_first_state=True)
self.assertTrue(self.cell_state.is_set_for_redraw())
self.cell_state.was_redrawn()
self.assertFalse(self.cell_state.is_set_for_redraw())
if __name__ == '__main__':
unittest.main()

View File

@ -1,87 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
sys.path.append('../cellular_automaton')
from cellular_automaton import *
import unittest
class TestRule(Rule):
def init_state(self, cell_coordinate):
return [1] if cell_coordinate == (1, 1) else [0]
def evolve_cell(self, last_cell_state, neighbors_last_states):
return [last_cell_state[0] + 1] if neighbors_last_states else last_cell_state
def get_state_draw_color(self, current_state):
return 255, 0, 0
class DrawEngineMock(display.DrawEngine):
written_texts = 0
filled_surfaces = 0
updated_rectangles = 0
_draws = 0
_draws_until_end = 1
def __init__(self, window_size=None, draws_until_end=1):
super(display.DrawEngine, self).__init__()
self._width = window_size[0]
self._height = window_size[1]
DrawEngineMock.written_texts = 0
DrawEngineMock.filled_surfaces = 0
DrawEngineMock.updated_rectangles = 0
DrawEngineMock._draws = 0
DrawEngineMock._draws_until_end = draws_until_end
def write_text(self, pos, text, color=(0, 255, 0)):
self.written_texts += 1
def fill_surface_with_color(self, rect, color=(0, 0, 0)):
self.filled_surfaces += 1
@staticmethod
def update_rectangles(rectangles):
DrawEngineMock.updated_rectangles += len(rectangles)
DrawEngineMock._draws += 1
@staticmethod
def is_active():
return DrawEngineMock._draws < DrawEngineMock._draws_until_end
class CAWindowMock(CAWindow, DrawEngineMock):
""" Mocks the window with fake engine. """
class TestDisplay(unittest.TestCase):
def setUp(self):
self.ca = CAFactory.make_single_process_cellular_automaton((3, 3), MooreNeighborhood(), TestRule)
def test_evolution_steps_per_draw(self):
mock = CAWindowMock(self.ca, evolution_steps_per_draw=10, window_size=(10, 10))
self.assertEqual(self.ca.get_current_evolution_step(), 10)
def test_updated_rectangle_count(self):
mock = CAWindowMock(self.ca, evolution_steps_per_draw=1, window_size=(10, 10), draws_until_end=4)
self.assertEqual(DrawEngineMock.updated_rectangles, 9 + 3)
if __name__ == '__main__':
unittest.main()

View File

@ -1,105 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
sys.path.append('../cellular_automaton')
from cellular_automaton import *
from cellular_automaton.cellular_automaton.cell_state import CellState
from cellular_automaton.cellular_automaton.state import CellularAutomatonState
import unittest
import mock
class TestFac(CAFactory):
@staticmethod
def make_cell_states(state_class, rule_, dimension):
return CAFactory._make_cell_states(state_class, rule_, dimension)
@staticmethod
def make_cells(cells, neighborhood_, dimension):
return CAFactory._make_cells(cells, neighborhood_, dimension)
@staticmethod
def make_cellular_automaton_state(dimension, neighborhood_, state_class, rule):
return TestFac._make_cellular_automaton_state(dimension, neighborhood_, state_class, rule)
class TestRule(Rule):
def evolve_cell(self, last_cell_state, neighbors_last_states):
return last_cell_state
def init_state(self, cell_coordinate):
return [1]
def get_state_draw_color(self, current_state):
return [0, 0, 0]
class TestCAFactory(unittest.TestCase):
def setUp(self):
self._neighborhood = MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS)
def test_make_ca_calls_correct_methods(self):
with mock.patch.object(CAFactory, '_make_cell_states', return_value={1: True}) as m1:
with mock.patch.object(CAFactory, '_make_cells') as m2:
TestFac.make_cellular_automaton_state([10], self._neighborhood, CellState, Rule)
m1.assert_called_once()
m2.assert_called_once_with({1: True}, self._neighborhood, [10])
def test_make_ca_returns_correct_values(self):
with mock.patch.object(CAFactory, '_make_cell_states', return_value={1: True}):
with mock.patch.object(CAFactory, '_make_cells', return_value={1: True}):
ca = TestFac.make_cellular_automaton_state([10], self._neighborhood, CellState, Rule)
self.assertIsInstance(ca, CellularAutomatonState)
self.assertEqual(tuple(ca.cells.values()), (True, ))
def test_make_cells(self):
cell_states = self.__create_cell_states()
cells = TestFac.make_cells(cell_states, self._neighborhood, [3, 3])
neighbours_of_mid = self.__cast_cells_to_list_and_remove_center_cell(cell_states)
self.assertEqual(set(cells[(1, 1)]._neighbor_states), set(neighbours_of_mid))
@staticmethod
def __cast_cells_to_list_and_remove_center_cell(cell_states):
neighbours_of_mid = list(cell_states.values())
neighbours_of_mid.remove(neighbours_of_mid[4])
return neighbours_of_mid
@staticmethod
def __create_cell_states():
cell_states = {}
for x in range(3):
for y in range(3):
cell_states[(x, y)] = CellState([x * y], False)
return cell_states
def test_1dimension_coordinates(self):
c = TestFac.make_cell_states(CellState, Rule(self._neighborhood), [3])
self.assertEqual(list(c.keys()), [(0,), (1,), (2,)])
def test_2dimension_coordinates(self):
c = TestFac.make_cell_states(CellState, Rule(self._neighborhood), [2, 2])
self.assertEqual(list(c.keys()), [(0, 0), (0, 1), (1, 0), (1, 1)])
def test_3dimension_coordinates(self):
c = TestFac.make_cell_states(CellState, Rule(self._neighborhood), [2, 2, 2])
self.assertEqual(list(c.keys()), [(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1),
(1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)])
if __name__ == '__main__':
unittest.main()

View File

@ -1,100 +0,0 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
sys.path.append('../cellular_automaton')
import cellular_automaton as csn
import unittest
class TestNeighborhood(unittest.TestCase):
@staticmethod
def check_neighbors(neighborhood, neighborhood_sets, dimension=(3, 3)):
for neighborhood_set in neighborhood_sets:
neighbors = neighborhood.calculate_cell_neighbor_coordinates(neighborhood_set[0], dimension)
if neighborhood_set[1] != neighbors:
print("\nrel_n:", neighborhood._rel_neighbors)
print("\nWrong neighbors (expected, real): ", (neighborhood_set[1]), neighbors)
return False
return True
def test_ignore_missing_neighbors(self):
neighborhood = csn.MooreNeighborhood(csn.EdgeRule.IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS)
n00 = [[0, 0], [[1, 0], [0, 1], [1, 1]]]
n01 = [[0, 1], [[0, 0], [1, 0], [1, 1], [0, 2], [1, 2]]]
n11 = [[1, 1], [[0, 0], [1, 0], [2, 0], [0, 1], [2, 1], [0, 2], [1, 2], [2, 2]]]
n22 = [[2, 2], [[1, 1], [2, 1], [1, 2]]]
self.assertTrue(self.check_neighbors(neighborhood, [n00, n01, n11, n22]))
def test_ignore_edge_cells(self):
neighborhood = csn.MooreNeighborhood(csn.EdgeRule.IGNORE_EDGE_CELLS)
n00 = [[0, 0], []]
n01 = [[0, 1], []]
n20 = [[2, 0], []]
n11 = [[1, 1], [[0, 0], [1, 0], [2, 0], [0, 1], [2, 1], [0, 2], [1, 2], [2, 2]]]
n22 = [[2, 2], []]
self.assertTrue(self.check_neighbors(neighborhood, [n00, n01, n20, n11, n22]))
def test_cyclic_dimensions(self):
neighborhood = csn.MooreNeighborhood(csn.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
n00 = [[0, 0], [[2, 2], [0, 2], [1, 2], [2, 0], [1, 0], [2, 1], [0, 1], [1, 1]]]
n11 = [[1, 1], [[0, 0], [1, 0], [2, 0], [0, 1], [2, 1], [0, 2], [1, 2], [2, 2]]]
n22 = [[2, 2], [[1, 1], [2, 1], [0, 1], [1, 2], [0, 2], [1, 0], [2, 0], [0, 0]]]
self.assertTrue(self.check_neighbors(neighborhood, [n00, n11, n22]))
def test_von_neumann_r1(self):
neighborhood = csn.VonNeumannNeighborhood(csn.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
n1 = [[1, 1], [[1, 0], [0, 1], [2, 1], [1, 2]]]
self.assertTrue(self.check_neighbors(neighborhood, [n1]))
def test_von_neumann_r2(self):
neighborhood = csn.VonNeumannNeighborhood(csn.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS, radius=2)
n1 = [[2, 2], [[2, 0], [1, 1], [2, 1], [3, 1], [0, 2], [1, 2], [3, 2], [4, 2], [1, 3], [2, 3], [3, 3], [2, 4]]]
self.assertTrue(self.check_neighbors(neighborhood, [n1], dimension=[5, 5]))
def test_von_neumann_d3(self):
neighborhood = csn.VonNeumannNeighborhood(csn.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS,
dimension=3)
n1 = [[1, 1, 1], [[1, 1, 0], [1, 0, 1], [0, 1, 1], [2, 1, 1], [1, 2, 1], [1, 1, 2]]]
self.assertTrue(self.check_neighbors(neighborhood, [n1], dimension=[3, 3, 3]))
def test_hexagonal(self):
neighborhood = csn.RadialNeighborhood(csn.EdgeRule.IGNORE_EDGE_CELLS, radius=2)
n1 = [[2, 2], [[1, 0], [2, 0], [3, 0],
[0, 1], [1, 1], [2, 1], [3, 1], [4, 1],
[0, 2], [1, 2], [3, 2], [4, 2],
[0, 3], [1, 3], [2, 3], [3, 3], [4, 3],
[1, 4], [2, 4], [3, 4]]]
self.assertTrue(self.check_neighbors(neighborhood, [n1], dimension=[5, 5]))
def test_hexagonal(self):
neighborhood = csn.HexagonalNeighborhood(csn.EdgeRule.IGNORE_EDGE_CELLS, radius=2)
n1 = [[2, 2], [[1, 0], [2, 0], [3, 0],
[0, 1], [1, 1], [2, 1], [3, 1],
[0, 2], [1, 2], [3, 2], [4, 2],
[0, 3], [1, 3], [2, 3], [3, 3],
[1, 4], [2, 4], [3, 4]]]
n2 = [[2, 3], [[1, 1], [2, 1], [3, 1],
[1, 2], [2, 2], [3, 2], [4, 2],
[0, 3], [1, 3], [3, 3], [4, 3],
[1, 4], [2, 4], [3, 4], [4, 4],
[1, 5], [2, 5], [3, 5]]]
self.assertTrue(self.check_neighbors(neighborhood, [n1, n2], dimension=[6, 6]))
if __name__ == '__main__':
unittest.main()

7
tests/context.py Normal file
View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import cellular_automaton

56
tests/test_automaton.py Normal file
View File

@ -0,0 +1,56 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# pylint: disable=missing-function-docstring
# pylint: disable=redefined-outer-name
import pytest
from .context import cellular_automaton as ca
class TAutomaton(ca.CellularAutomaton):
""" Simple Automaton for test purposes """
def evolve_rule(self, last_cell_state, neighbors_last_states):
return [last_cell_state[0] + 1]
def init_cell_state(self, cell_coordinate):
return [0]
NEIGHBORHOOD = ca.MooreNeighborhood(ca.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
@pytest.fixture
def automaton():
return TAutomaton(NEIGHBORHOOD, [3, 3])
def test_process_evolution_steps(automaton):
automaton.evolve(5)
assert automaton.evolution_step == 5
def test_process_evolution_calls(automaton):
automaton.evolve(5)
assert automaton.cells[(1, 1)].state[0] == 5
@pytest.mark.parametrize("dimensions", [1, 2, 3, 4, 5])
def test_dimensions(dimensions):
automaton = TAutomaton(ca.MooreNeighborhood(), dimension=[3] * dimensions)
automaton.evolve()
assert automaton.cells[(1, ) * dimensions].state[0] == 1

72
tests/test_display.py Normal file
View File

@ -0,0 +1,72 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# pylint: disable=missing-function-docstring
# pylint: disable=missing-class-docstring
# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
from unittest.mock import MagicMock
import sys
import pytest
from .context import cellular_automaton as ca
def import_mock(module):
def argument_wrapper(func):
def function_wrapper(automaton, *args, **kwargs):
try:
module_ = sys.modules[module]
except KeyError:
module_ = ""
sys.modules[module] = MagicMock()
return_value = func(automaton, pygame_mock=sys.modules[module], *args, **kwargs)
if module_ != "":
sys.modules[module] = module_
else:
del sys.modules[module]
return return_value
return function_wrapper
return argument_wrapper
class TAutomaton(ca.CellularAutomaton):
def init_cell_state(self, cell_coordinate):
return [1] if cell_coordinate == (1, 1) else [0]
def evolve_rule(self, last_cell_state, neighbors_last_states):
return [last_cell_state[0] + 1] if neighbors_last_states else last_cell_state
@pytest.fixture
def automaton():
return TAutomaton(ca.MooreNeighborhood(), (3, 3))
@import_mock(module='pygame')
def test_evolution_steps_per_draw(automaton, pygame_mock):
ca.CAWindow(cellular_automaton=automaton, window_size=(10, 10)).run(evolutions_per_draw=10, last_evolution_step=1)
assert automaton.evolution_step == 10
@import_mock(module='pygame')
def test_updated_rectangle_calls(automaton, pygame_mock):
ca.CAWindow(cellular_automaton=automaton, window_size=(10, 10)).run(last_evolution_step=4)
assert pygame_mock.display.update.call_count == 4 * (3 + 1) # steps * (texts + changed cells)

33
tests/test_factory.py Normal file
View File

@ -0,0 +1,33 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from .context import cellular_automaton as ca
class TAutomaton(ca.CellularAutomaton):
def evolve_rule(self, last_cell_state, neighbors_last_states):
return last_cell_state
def init_cell_state(self, cell_coordinate):
return cell_coordinate
def test_ca_has_correct_values():
ca_ = TAutomaton(ca.MooreNeighborhood(), [3, 3])
assert tuple(c.state for c in ca_.cells.values()) == ((0, 0), (0, 1), (0, 2),
(1, 0), (1, 1), (1, 2),
(2, 0), (2, 1), (2, 2))

148
tests/test_neighborhood.py Normal file
View File

@ -0,0 +1,148 @@
"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# pylint: disable=missing-function-docstring
# pylint: disable=redefined-outer-name
import pytest
from .context import cellular_automaton as ca
def check_neighbors(neighborhood, neighborhood_sets, dimension=(3, 3)):
for neighborhood_set in neighborhood_sets:
neighbors = neighborhood.calculate_cell_neighbor_coordinates(neighborhood_set(0), dimension)
if neighborhood_set(1) != neighbors:
print("\nWrong neighbors (expected, real): ", (neighborhood_set(1)), neighbors)
return False
return True
@pytest.mark.parametrize(('coordinate', 'expected_neighborhood'),
(((0, 0), ((1, 0), (0, 1), (1, 1))),
((0, 1), ((0, 0), (1, 0), (1, 1), (0, 2), (1, 2))),
((1, 1), ((0, 0), (1, 0), (2, 0), (0, 1), (2, 1), (0, 2), (1, 2), (2, 2))),
((2, 2), ((1, 1), (2, 1), (1, 2)))))
def test_ignore_missing_neighbors(coordinate, expected_neighborhood):
neighborhood = ca.MooreNeighborhood(ca.EdgeRule.IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS)
actual_neighborhood = neighborhood.calculate_cell_neighbor_coordinates(coordinate, (3, 3))
assert actual_neighborhood == expected_neighborhood
@pytest.mark.parametrize(('coordinate', 'expected_neighborhood'),
(((0, 0), ()),
((0, 1), ()),
((2, 0), ()),
((1, 1), ((0, 0), (1, 0), (2, 0), (0, 1), (2, 1), (0, 2), (1, 2), (2, 2))),
((2, 2), ())))
def test_ignore_edge_cells(coordinate, expected_neighborhood):
neighborhood = ca.MooreNeighborhood()
actual_neighborhood = neighborhood.calculate_cell_neighbor_coordinates(coordinate, (3, 3))
assert actual_neighborhood == expected_neighborhood
@pytest.mark.parametrize(('coordinate', 'expected_neighborhood'),
(((0, 0), ((2, 2), (0, 2), (1, 2), (2, 0), (1, 0), (2, 1), (0, 1), (1, 1))),
((1, 1), ((0, 0), (1, 0), (2, 0), (0, 1), (2, 1), (0, 2), (1, 2), (2, 2))),
((2, 2), ((1, 1), (2, 1), (0, 1), (1, 2), (0, 2), (1, 0), (2, 0), (0, 0)))))
def test_cyclic_dimensions(coordinate, expected_neighborhood):
neighborhood = ca.MooreNeighborhood(ca.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
actual_neighborhood = neighborhood.calculate_cell_neighbor_coordinates(coordinate, (3, 3))
assert actual_neighborhood == expected_neighborhood
def test_von_neumann_r1():
neighborhood = ca.VonNeumannNeighborhood(ca.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
actual_neighborhood = neighborhood.calculate_cell_neighbor_coordinates((1, 1), (3, 3))
assert actual_neighborhood == ((1, 0), (0, 1), (2, 1), (1, 2))
def test_von_neumann_r2():
neighborhood = ca.VonNeumannNeighborhood(ca.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS, radius=2)
actual_neighborhood = neighborhood.calculate_cell_neighbor_coordinates((2, 2), (5, 5))
assert actual_neighborhood == ((2, 0), (1, 1), (2, 1), (3, 1), (0, 2), (1, 2),
(3, 2), (4, 2), (1, 3), (2, 3), (3, 3), (2, 4))
def test_von_neumann_d3():
neighborhood = ca.VonNeumannNeighborhood(ca.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
actual_neighborhood = neighborhood.calculate_cell_neighbor_coordinates((1, 1, 1), (3, 3, 3))
assert actual_neighborhood == ((1, 1, 0), (1, 0, 1), (0, 1, 1), (2, 1, 1), (1, 2, 1), (1, 1, 2))
def test_radial():
neighborhood = ca.RadialNeighborhood(radius=2)
actual_neighborhood = neighborhood.calculate_cell_neighbor_coordinates((2, 2), (5, 5))
assert actual_neighborhood == ((1, 0), (2, 0), (3, 0),
(0, 1), (1, 1), (2, 1), (3, 1), (4, 1),
(0, 2), (1, 2), (3, 2), (4, 2),
(0, 3), (1, 3), (2, 3), (3, 3), (4, 3),
(1, 4), (2, 4), (3, 4))
@pytest.mark.parametrize(('coordinate', 'expected_neighborhood'),
(((2, 2), ((1, 0), (2, 0), (3, 0),
(0, 1), (1, 1), (2, 1), (3, 1),
(0, 2), (1, 2), (3, 2), (4, 2),
(0, 3), (1, 3), (2, 3), (3, 3),
(1, 4), (2, 4), (3, 4))),
((2, 3), ((1, 1), (2, 1), (3, 1),
(1, 2), (2, 2), (3, 2), (4, 2),
(0, 3), (1, 3), (3, 3), (4, 3),
(1, 4), (2, 4), (3, 4), (4, 4),
(1, 5), (2, 5), (3, 5)))))
def test_hexagonal(coordinate, expected_neighborhood):
neighborhood = ca.HexagonalNeighborhood(radius=2)
actual_neighborhood = neighborhood.calculate_cell_neighbor_coordinates(coordinate, (6, 6))
assert actual_neighborhood == expected_neighborhood
@pytest.mark.parametrize(('coordinate', 'cid'),
(((-1, -1), 0),
((0, -1), 1),
((1, -1), 2),
((-1, 0), 3),
((1, 0), 4),
((-1, 1), 5),
((0, 1), 6),
((1, 1), 7)))
def test_get_neighbour_by_relative(coordinate, cid):
neighborhood = ca.MooreNeighborhood()
neighborhood.calculate_cell_neighbor_coordinates((0, 0), [3, 3])
assert neighborhood.get_neighbor_by_relative_coordinate(list(range(8)), coordinate) == cid
@pytest.mark.parametrize("dimensions", (1, 2, 3, 4, 5))
def test_get_relative_11_neighbor_of_coordinate_11(dimensions):
neighborhood = ca.MooreNeighborhood()
neighbor = neighborhood.get_neighbor_by_relative_coordinate(
neighborhood.calculate_cell_neighbor_coordinates((1,)*dimensions, (3,)*dimensions),
(1,)*dimensions)
assert neighbor == (2, )*dimensions
@pytest.mark.parametrize(
("dimension", "expected"),
((1, ((0, ), (2, ))),
(2, ((0, 0), (1, 0), (2, 0),
(0, 1), (2, 1),
(0, 2), (1, 2), (2, 2))),
(3, ((0, 0, 0), (1, 0, 0), (2, 0, 0), (0, 1, 0), (1, 1, 0), (2, 1, 0), (0, 2, 0), (1, 2, 0), (2, 2, 0),
(0, 0, 1), (1, 0, 1), (2, 0, 1), (0, 1, 1), (2, 1, 1), (0, 2, 1), (1, 2, 1), (2, 2, 1),
(0, 0, 2), (1, 0, 2), (2, 0, 2), (0, 1, 2), (1, 1, 2), (2, 1, 2), (0, 2, 2), (1, 2, 2), (2, 2, 2)))))
def test_get_neighbor_coordinates(dimension, expected):
n = ca.MooreNeighborhood(edge_rule=ca.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
assert n.calculate_cell_neighbor_coordinates((1,) * dimension, (3,) * dimension) == expected