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/
build/
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:
- easy to use
- n dimensional
- multi process capable
- speed optimized
- documented
- 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.
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
This module can be loaded and installed from [pipy](https://pypi.org/project/cellular-automaton/): `pip install cellular-automaton`
## 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 dimensions of the grid
- The evolution rule
- The initial cell state
`````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)
ca = CAFactory.make_single_process_cellular_automaton(dimension=[100, 100],
neighborhood=neighborhood,
rule=MyRule)
ca = MyCellularAutomaton(dimension=[100, 100],
neighborhood=neighborhood)
``````
### 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.
### Rule
The Rule has three tasks:
- Set the initial value for all cells.
- Evolve a cell in respect to its neighbours.
- (optional) define how the cell should be drawn.
`````python
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.
### Evolution and Initial State
To define the evolution rule and the initial state create a class inheriting from `CellularAutomaton`.
- The `init_cell_state` method will be called once during the creation process for every cell.
It will get the coordinate of that cell and is supposed to return a tuple representing that cells state.
- 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.
All new states will be applied simultaneously, so the order of processing the cells is irrelevant.
## 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.
## 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)
- [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
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!
## 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
[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

View File

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

View File

@ -1,5 +1,21 @@
from .neighborhood import Neighborhood, MooreNeighborhood, VonNeumannNeighborhood, \
EdgeRule, HexagonalNeighborhood, RadialNeighborhood
from .rule import Rule
from .factory import CAFactory
#!/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.
"""
from .neighborhood import Neighborhood, MooreNeighborhood, RadialNeighborhood, VonNeumannNeighborhood, \
HexagonalNeighborhood, EdgeRule
from .automaton import CellularAutomaton
from .display import CAWindow

View File

@ -14,91 +14,122 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
import multiprocessing
from typing import Sequence
from multiprocessing.sharedctypes import RawValue
from ctypes import c_int
import abc
import itertools
import recordclass
from cellular_automaton import Neighborhood
class CellularAutomatonProcessor:
""" This class is responsible for the evolution of the cells. """
CELL = recordclass.make_dataclass("Cell",
("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):
""" Evolve all cells x times.
:param x: The number of evolution steps processed with the call of this method.
"""
for x in range(x):
self.evolve()
class CellularAutomatonCreator(abc.ABC):
""" Creates a cellular automaton from a dimension and a neighborhood definition """
def evolve(self):
""" Evolve all cells """
self._ca.current_evolution_step += 1
i = self._ca.current_evolution_step
r = self._ca.evolution_rule.evolve_cell
list(map(lambda c: c.evolve_if_ready(r, i), tuple(self._ca.cells.values())))
def __init__(self,
dimension,
neighborhood: Neighborhood,
*args, **kwargs):
super().__init__(*args, **kwargs)
self._dimension = dimension
self._neighborhood = neighborhood
self._current_state = {}
self._next_state = {}
self.__make_cellular_automaton_state()
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):
return self._ca.cells
return self._current_state
def get_current_evolution_step(self):
return self._ca.current_evolution_step
cells = property(get_cells)
def get_current_rule(self):
return self._ca.evolution_rule
def get_evolution_step(self):
return self._evolution_step
evolution_step = property(get_evolution_step)
class CellularAutomatonMultiProcessor(CellularAutomatonProcessor):
""" This is a variant of CellularAutomatonProcessor that uses multi processing.
The evolution of the cells will be outsourced to new processes.
def evolve(self, times=1):
""" Evolve all cells x times.
:param times: The number of evolution steps processed with one call of this method.
"""
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
WARNING:
This variant has high memory use!
The inter process communication overhead can make this variant slower than single processing!
"""
def __evolve_cells(self, this_state, next_state):
evolve_cell = self.__evolve_cell
evolution_rule = self.evolve_rule
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)
def __init__(self, cellular_automaton, process_count: int = 2):
multiprocessing.freeze_support()
if process_count < 1:
raise ValueError
@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
super().__init__(cellular_automaton)
self.evolve_range = range(len(self._ca.cells))
self._ca.current_evolution_step = RawValue(c_int, self._ca.current_evolution_step)
self.__init_processes(process_count)
def __init_processes(self, process_count):
self.pool = multiprocessing.Pool(processes=process_count,
initializer=_init_process,
initargs=(tuple(self._ca.cells.values()),
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)
def evolve_rule(self, last_cell_state: Sequence, neighbors_last_states: Sequence) -> Sequence: # pragma: no cover
""" 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
"""
raise NotImplementedError

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.
"""
# pylint: disable=all
import time
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:
def __init__(self, left=0, top=0, width=0, height=0, rect=None, pos=None, size=None):
if rect is not None and (pos is not None or size is not None):
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")
class PygameEngine:
""" This is an wrapper for the pygame engine.
By initializing pygame lazy the dependency can be dropped.
"""
self.__direct_initialisation(height, left, top, width)
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):
def __init__(self, window_size, *args, **kwargs):
super().__init__(*args, **kwargs)
global pygame
import pygame
pygame.init()
self._pygame = pygame
self._pygame.init()
pygame.display.set_caption("Cellular Automaton")
self.__screen = pygame.display.set_mode(window_size)
self.__font = pygame.font.SysFont("monospace", 15)
@ -70,93 +47,110 @@ class DrawEngine(object):
def write_text(self, pos, text, color=(0, 255, 0)):
label = self.__font.render(text, 1, color)
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)):
return self.__screen.fill(color, rect)
@staticmethod
def update_rectangles(rectangles):
pygame.display.update(rectangles)
def update_rectangles(self, rectangles):
self._pygame.display.update(rectangles)
@staticmethod
def is_active():
for event in pygame.event.get():
if event.type == pygame.QUIT:
def is_active(self): # pragma: no cover
for event in self._pygame.event.get():
if event.type == self._pygame.QUIT:
return False
return True
class _CASurface:
class CAWindow:
def __init__(self,
grid_rect,
cellular_automaton: automaton.CellularAutomatonProcessor,
draw_engine,
cellular_automaton: CellularAutomaton,
window_size=(1000, 800),
draw_engine=None,
state_to_color_cb=None,
*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)
self._cellular_automaton = cellular_automaton
self.__rect = grid_rect
self.__cell_size = self._calculate_cell_display_size()
self.__draw_engine = draw_engine
self.__rect = _Rect(left=0, top=30, width=window_size[0], height=window_size[1] - 30)
self.__calculate_cell_display_size()
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):
grid_dimension = self._cellular_automaton.get_dimension()
return [self.__rect.width / grid_dimension[0], self.__rect.height / grid_dimension[1]]
def run(self,
evolutions_per_second=0,
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):
""" Redraws those cells which changed their state since last redraw. """
def _sleep_to_keep_rate(self, time_taken, evolutions_per_second): # pragma: no cover
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()))
def __redraw_dirty_cells(self):
for coordinate, cell in self._cellular_automaton.get_cells().items():
if cell.is_set_for_redraw():
yield from self.__redraw_cell(cell, coordinate)
for coordinate, cell in self._cellular_automaton.cells.items():
if cell.is_dirty:
yield self.__redraw_cell(cell, coordinate)
def __redraw_cell(self, cell, coordinate):
cell_color = self.__get_cell_color(cell)
cell_pos = self._calculate_cell_position_in_the_grid(coordinate)
surface_pos = self._calculate_cell_position_on_screen(cell_pos)
cell.was_redrawn()
yield self._draw_cell_surface(surface_pos, cell_color)
cell_color = self.__state_to_color(cell.state)
cell_pos = self.__calculate_cell_position_in_the_grid(coordinate)
surface_pos = self.__calculate_cell_position_on_screen(cell_pos)
cell.is_dirty = False
return self.__draw_cell_surface(surface_pos, cell_color)
def __get_cell_color(self, cell):
return self._cellular_automaton.get_current_rule().get_state_draw_color(
cell.get_current_state(self._cellular_automaton.get_current_evolution_step()))
def _get_cell_color(self, current_state: Sequence) -> Sequence:
""" Returns the color of the cell depending on its current state """
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))
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]]
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)
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 __init__(self, cellular_automaton: automaton.CellularAutomatonProcessor,
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()))
def _is_not_user_terminated(self):
return self.__draw_engine.is_active()

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):
""" Enum for different possibilities to handle the edge of the automaton. """
IGNORE_EDGE_CELLS = 0
IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS = 1
FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS = 2
class Neighborhood:
def __init__(self, neighbors_relative, edge_rule: EdgeRule):
""" Defines a neighborhood of a cell.
:param neighbors_relative: List of relative coordinates for cell neighbors.
:param edge_rule: EdgeRule to define, how cells on the edge of the grid will be handled.
""" Defines which cells should be considered to be neighbors during evolution of cellular automaton."""
def __init__(self, edge_rule=EdgeRule.IGNORE_EDGE_CELLS, radius=1):
""" 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.__grid_dimensions = []
def calculate_cell_neighbor_coordinates(self, cell_coordinate, grid_dimensions):
""" 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.
:return: list of absolute coordinates for the cells neighbors.
"""
self.__grid_dimensions = grid_dimensions
return list(self._neighbors_generator(cell_coordinate))
self.__lazy_initialize_relative_neighborhood(grid_dimensions)
return tuple(self._neighbors_generator(cell_coordinate))
def get_id_of_neighbor_from_relative_coordinate(self, rel_coordinate):
return self._rel_neighbors.index(rel_coordinate)
def __lazy_initialize_relative_neighborhood(self, grid_dimensions):
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):
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:
yield from self._calculate_abs_neighbor_and_decide_validity(cell_coordinate, rel_n)
def _calculate_abs_neighbor_and_decide_validity(self, cell_coordinate, rel_n):
n = list(map(operator.add, rel_n, cell_coordinate))
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
def _does_ignore_edge_cell_rule_apply(self, coordinate):
return self.__edge_rule == EdgeRule.IGNORE_EDGE_CELLS and self.__is_coordinate_on_an_edge(coordinate)
if on_edge:
n, n_folded = zip(*[(ni + ci, (ni + di + ci) % di)
for ci, ni, di in zip(cell_coordinate, rel_n, self._grid_dimensions)])
if self.__edge_rule == EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS or n == n_folded:
yield n_folded
else:
yield tuple(map(operator.add, rel_n, cell_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))
def __apply_edge_overflow(self, n):
return list(map(lambda ni, di: (ni + di) % di, n, self.__grid_dimensions))
return any(ci in [0, di-1] for ci, di in zip(coordinate, self._grid_dimensions))
class MooreNeighborhood(Neighborhood):
@ -86,10 +102,6 @@ class MooreNeighborhood(Neighborhood):
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):
""" 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
"""
def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, radius=1, dimension=2):
self.radius = radius
super().__init__(tuple(_rel_neighbor_generator(dimension, radius, self.neighbor_rule)),
edge_rule)
def neighbor_rule(self, rel_n):
def _neighbor_rule(self, rel_neighbor):
cross_sum = 0
for ci in rel_n:
cross_sum += abs(ci)
return cross_sum <= self.radius
for coordinate_i in rel_neighbor:
cross_sum += abs(coordinate_i)
return cross_sum <= self._radius
class RadialNeighborhood(Neighborhood):
@ -139,18 +146,16 @@ class RadialNeighborhood(Neighborhood):
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):
self.radius = radius
def __init__(self, *args, delta_=.25, **kwargs):
self.delta = delta_
super().__init__(tuple(_rel_neighbor_generator(dimension, radius, self.neighbor_rule)),
edge_rule)
super().__init__(*args, **kwargs)
def neighbor_rule(self, rel_n):
def _neighbor_rule(self, rel_neighbor):
cross_sum = 0
for ci in rel_n:
cross_sum += pow(ci, 2)
for coordinate_i in rel_neighbor:
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):
@ -186,30 +191,27 @@ class HexagonalNeighborhood(Neighborhood):
X N N N X X N N N X
"""
def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, radius=1):
neighbor_lists = [[(0, 0)],
[(0, 0)]]
def __init__(self, *args, radius=1, **kwargs):
super().__init__(radius=radius, *args, **kwargs)
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 __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):
def get_neighbor_by_relative_coordinate(self, neighbors, rel_coordinate): # pragma: no cover
raise NotImplementedError
def _neighbors_generator(self, cell_coordinate):
if not self._does_ignore_edge_cell_rule_apply(cell_coordinate):
for rel_n in self._rel_neighbors[cell_coordinate[1] % 2]:
yield from self._calculate_abs_neighbor_and_decide_validity(cell_coordinate, rel_n)
def calculate_cell_neighbor_coordinates(self, cell_coordinate, grid_dimensions):
self._rel_neighbors = self._neighbor_lists[cell_coordinate[1] % 2]
return super().calculate_cell_neighbor_coordinates(cell_coordinate, grid_dimensions)
@staticmethod
def __add_rectangular_neighbours(neighbours, radius, is_odd):
@ -226,12 +228,6 @@ class HexagonalNeighborhood(Neighborhood):
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):
new_neighbours = 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.
"""
import random
from cellular_automaton import *
# pylint: disable=wrong-import-position
# 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]
DEAD = [0]
class ConwaysRule(Rule):
random_seed = random.seed(13)
class ConwaysCA(CellularAutomaton):
""" 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)
init = max(.0, float(rand - 14))
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
alive_neighbours = self.__count_alive_neighbours(neighbors_last_states)
if last_cell_state == DEAD and alive_neighbours == 3:
@ -46,20 +57,13 @@ class ConwaysRule(Rule):
@staticmethod
def __count_alive_neighbours(neighbours):
an = []
alive_neighbors = []
for n in neighbours:
if n == ALIVE:
an.append(1)
return len(an)
def get_state_draw_color(self, current_state):
return [255 if current_state[0] else 0, 0, 0]
alive_neighbors.append(1)
return len(alive_neighbors)
if __name__ == "__main__":
neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
ca = CAFactory.make_multi_process_cellular_automaton(dimension=[100, 100],
neighborhood=neighborhood,
rule=ConwaysRule,
processes=4)
ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1)
with contextlib.suppress(KeyboardInterrupt):
CAWindow(cellular_automaton=ConwaysCA()).run(evolutions_per_second=40)

View File

@ -15,29 +15,43 @@ 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 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):
""" A basic cellular automaton that just copies one neighbour state so get some motion in the grid. """
random_seed = random.seed(1000)
class StarFallAutomaton(CellularAutomaton):
""" Represents an automaton dropping colorful stars """
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)
init = max(.0, float(rand - 99))
return [init]
return [init * random.randint(0, 3)]
def evolve_cell(self, last_cell_state, neighbors_last_states):
return self._get_neighbor_by_relative_coordinate(neighbors_last_states, (-1, -1))
def evolve_rule(self, __, neighbors_last_states: Sequence) -> Sequence:
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__":
neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
ca = CAFactory.make_single_process_cellular_automaton(dimension=[100, 100],
neighborhood=neighborhood,
rule=StarfallRule)
ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1)
with contextlib.suppress(KeyboardInterrupt):
CAWindow(cellular_automaton=StarFallAutomaton(), state_to_color_cb=state_to_color).run()

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:
long_description = f.read()
setup(
name="cellular_automaton",
version="0.1.0",
version="1.0.0",
author="Richard Feistenauer",
author_email="r.feistenauer@web.de",
packages=["cellular_automaton"],
packages=find_packages(exclude=('tests', 'docs', 'examples')),
url="https://gitlab.com/DamKoVosh/cellular_automaton",
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_content_type='text/markdown',
requires=[""],
python_requires='>3.6.1'
requires=["Python (>3.6.1)", "recordclass", "pytest"]
)

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