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:
parent
4ab4b2a6d2
commit
9b527a044f
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@ dist/
|
||||
cellular_automaton.egg-info/
|
||||
build/
|
||||
MANIFEST
|
||||
timing.log
|
23
.gitlab-ci.yml
Normal file
23
.gitlab-ci.yml
Normal 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
10
.pylintrc
Normal 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
|
74
README.md
74
README.md
@ -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
|
||||
|
@ -1 +0,0 @@
|
||||
from cellular_automaton.cellular_automaton import *
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
WARNING:
|
||||
This variant has high memory use!
|
||||
The inter process communication overhead can make this variant slower than single processing!
|
||||
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
|
||||
|
||||
def __init__(self, cellular_automaton, process_count: int = 2):
|
||||
multiprocessing.freeze_support()
|
||||
if process_count < 1:
|
||||
raise ValueError
|
||||
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)
|
||||
|
||||
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))
|
||||
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
|
||||
|
@ -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)
|
@ -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
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
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
|
||||
|
||||
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)
|
||||
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:
|
||||
|
@ -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]
|
@ -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
|
@ -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)
|
||||
|
@ -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
63
examples/times.py
Normal 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)')
|
13
setup.py
13
setup.py
@ -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"]
|
||||
)
|
||||
|
@ -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()
|
@ -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()
|
@ -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()
|
@ -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()
|
@ -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()
|
@ -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
7
tests/context.py
Normal 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
56
tests/test_automaton.py
Normal 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
72
tests/test_display.py
Normal 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
33
tests/test_factory.py
Normal 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
148
tests/test_neighborhood.py
Normal 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
|
Loading…
Reference in New Issue
Block a user