diff --git a/.gitignore b/.gitignore index d6188b0..f5423ca 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ performance.txt dist/ cellular_automaton.egg-info/ build/ -MANIFEST \ No newline at end of file +MANIFEST +timing.log \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..3b4f4c2 --- /dev/null +++ b/.gitlab-ci.yml @@ -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}%)/' diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..423bd6c --- /dev/null +++ b/.pylintrc @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 3486b16..e5a169a 100644 --- a/README.md +++ b/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 diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 4dbcc84..0000000 --- a/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from cellular_automaton.cellular_automaton import * diff --git a/cellular_automaton/__init__.py b/cellular_automaton/__init__.py index b2b8959..2698fb5 100644 --- a/cellular_automaton/__init__.py +++ b/cellular_automaton/__init__.py @@ -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 diff --git a/cellular_automaton/automaton.py b/cellular_automaton/automaton.py index 8397d5b..d649ccc 100644 --- a/cellular_automaton/automaton.py +++ b/cellular_automaton/automaton.py @@ -14,91 +14,122 @@ See the License for the specific language governing permissions and limitations under the License. """ -import multiprocessing +from typing import Sequence -from multiprocessing.sharedctypes import RawValue -from ctypes import c_int +import abc +import itertools +import recordclass + +from cellular_automaton import Neighborhood -class CellularAutomatonProcessor: - """ This class is responsible for the evolution of the cells. """ +CELL = recordclass.make_dataclass("Cell", + ("state", "is_active", "is_dirty", "neighbors"), + defaults=((0, ), True, True, (None, ))) - def __init__(self, cellular_automaton): - self._ca = cellular_automaton - def evolve_x_times(self, x): - """ Evolve all cells x times. - :param x: The number of evolution steps processed with the call of this method. - """ - for x in range(x): - self.evolve() +class CellularAutomatonCreator(abc.ABC): + """ Creates a cellular automaton from a dimension and a neighborhood definition """ - def evolve(self): - """ Evolve all cells """ - self._ca.current_evolution_step += 1 - i = self._ca.current_evolution_step - r = self._ca.evolution_rule.evolve_cell - list(map(lambda c: c.evolve_if_ready(r, i), tuple(self._ca.cells.values()))) + def __init__(self, + dimension, + neighborhood: Neighborhood, + *args, **kwargs): + super().__init__(*args, **kwargs) + self._dimension = dimension + self._neighborhood = neighborhood + + self._current_state = {} + self._next_state = {} + self.__make_cellular_automaton_state() def get_dimension(self): - return self._ca.dimension + return self._dimension + + dimension = property(get_dimension) + + def __make_cellular_automaton_state(self): + self.__make_cells() + self.__add_neighbors() + + def __make_cells(self): + for coord in itertools.product(*[range(d) for d in self._dimension]): + self._current_state[coord] = CELL(self.init_cell_state(coord)) + self._next_state[coord] = CELL(self.init_cell_state(coord)) + + def __add_neighbors(self): + calculate_neighbor_coordinates = self._neighborhood.calculate_cell_neighbor_coordinates + coordinates = self._current_state.keys() + for coordinate, cell_c, cell_n in zip(coordinates, self._current_state.values(), self._next_state.values()): + n_coord = calculate_neighbor_coordinates(coordinate, self._dimension) + cell_c.neighbors = list([self._current_state[nc] for nc in n_coord]) + cell_n.neighbors = list([self._next_state[nc] for nc in n_coord]) + + def init_cell_state(self, cell_coordinate: Sequence) -> Sequence: # pragma: no cover + """ Will be called to initialize a cells state. + :param cell_coordinate: Cells coordinate. + :return: Iterable that represents the initial cell state + """ + raise NotImplementedError + + +class CellularAutomaton(CellularAutomatonCreator, abc.ABC): + """ + This class represents a cellular automaton. + It can be created with n dimensions and can handle different neighborhood definitions. + + :param dimension: Iterable of len = dimensions + (e.g. [4, 3, 3, 3] = 4 x 3 x 3 x 3 cells in a four dimensional cube). + :param neighborhood: Defines which cells are considered neighbors. + """ + def __init__(self, neighborhood: Neighborhood, *args, **kwargs): + super().__init__(neighborhood=neighborhood, *args, **kwargs) + self._evolution_step = 0 def get_cells(self): - return self._ca.cells + return self._current_state - def get_current_evolution_step(self): - return self._ca.current_evolution_step + cells = property(get_cells) - def get_current_rule(self): - return self._ca.evolution_rule + def get_evolution_step(self): + return self._evolution_step + evolution_step = property(get_evolution_step) -class CellularAutomatonMultiProcessor(CellularAutomatonProcessor): - """ This is a variant of CellularAutomatonProcessor that uses multi processing. - The evolution of the cells will be outsourced to new processes. + def evolve(self, times=1): + """ Evolve all cells x times. + :param times: The number of evolution steps processed with one call of this method. + """ + for _ in itertools.repeat(None, times): + self.__evolve_cells(self._current_state, self._next_state) + self._current_state, self._next_state = self._next_state, self._current_state + self._evolution_step += 1 - WARNING: - This variant has high memory use! - The inter process communication overhead can make this variant slower than single processing! - """ + def __evolve_cells(self, this_state, next_state): + evolve_cell = self.__evolve_cell + evolution_rule = self.evolve_rule + for old, new in zip(this_state.values(), next_state.values()): + if old.is_active: + new_state = evolution_rule(old.state, [n.state for n in old.neighbors]) + old.is_active = False + evolve_cell(old, new, new_state) - def __init__(self, cellular_automaton, process_count: int = 2): - multiprocessing.freeze_support() - if process_count < 1: - raise ValueError + @classmethod + def __evolve_cell(cls, old, cell, new_state): + changed = new_state != cell.state + cell.state = new_state + cell.is_dirty |= changed + old.is_dirty |= changed + if changed: + cell.is_active = True + for n in cell.neighbors: + n.is_active = True - super().__init__(cellular_automaton) - - self.evolve_range = range(len(self._ca.cells)) - self._ca.current_evolution_step = RawValue(c_int, self._ca.current_evolution_step) - self.__init_processes(process_count) - - def __init_processes(self, process_count): - self.pool = multiprocessing.Pool(processes=process_count, - initializer=_init_process, - initargs=(tuple(self._ca.cells.values()), - self._ca.evolution_rule, - self._ca.current_evolution_step)) - - def evolve(self): - self._ca.current_evolution_step.value += 1 - self.pool.map(_process_routine, self.evolve_range) - - def get_current_evolution_step(self): - return self._ca.current_evolution_step.value - - -global_cells = None -global_rule = None -global_evolution_step = None - - -def _init_process(cells, rule, index): - global global_rule, global_cells, global_evolution_step - global_cells = cells - global_rule = rule - global_evolution_step = index - - -def _process_routine(i): - global_cells[i].evolve_if_ready(global_rule.evolve_cell, global_evolution_step.value) + def evolve_rule(self, last_cell_state: Sequence, neighbors_last_states: Sequence) -> Sequence: # pragma: no cover + """ Calculates and sets new state of 'cell'. + A cells evolution will only be called if it or at least one of its neighbors has changed last evolution_step. + :param last_cell_state: The cells state previous to the evolution step. + :param neighbors_last_states: The cells neighbors current states. + :return: New state. The state after this evolution step + """ + raise NotImplementedError diff --git a/cellular_automaton/cell.py b/cellular_automaton/cell.py deleted file mode 100644 index 9793721..0000000 --- a/cellular_automaton/cell.py +++ /dev/null @@ -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) diff --git a/cellular_automaton/cell_state.py b/cellular_automaton/cell_state.py deleted file mode 100644 index db6e707..0000000 --- a/cellular_automaton/cell_state.py +++ /dev/null @@ -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 diff --git a/cellular_automaton/display.py b/cellular_automaton/display.py index f521e5a..c9ef8a1 100644 --- a/cellular_automaton/display.py +++ b/cellular_automaton/display.py @@ -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() diff --git a/cellular_automaton/factory.py b/cellular_automaton/factory.py deleted file mode 100644 index 079504b..0000000 --- a/cellular_automaton/factory.py +++ /dev/null @@ -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 - - diff --git a/cellular_automaton/neighborhood.py b/cellular_automaton/neighborhood.py index ee9ee08..05be418 100644 --- a/cellular_automaton/neighborhood.py +++ b/cellular_automaton/neighborhood.py @@ -21,20 +21,25 @@ import math class EdgeRule(enum.Enum): + """ Enum for different possibilities to handle the edge of the automaton. """ IGNORE_EDGE_CELLS = 0 IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS = 1 FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS = 2 class Neighborhood: - def __init__(self, neighbors_relative, edge_rule: EdgeRule): - """ Defines a neighborhood of a cell. - :param neighbors_relative: List of relative coordinates for cell neighbors. - :param edge_rule: EdgeRule to define, how cells on the edge of the grid will be handled. + """ Defines which cells should be considered to be neighbors during evolution of cellular automaton.""" + + def __init__(self, edge_rule=EdgeRule.IGNORE_EDGE_CELLS, radius=1): + """ General class for all Neighborhoods. + :param edge_rule: Rule to define, how cells on the edge of the grid will be handled. + :param radius: If radius > 1 it grows the neighborhood + by adding the neighbors of the neighbors radius times. """ - self._rel_neighbors = neighbors_relative + self._rel_neighbors = None + self._grid_dimensions = [] + self._radius = radius self.__edge_rule = edge_rule - self.__grid_dimensions = [] def calculate_cell_neighbor_coordinates(self, cell_coordinate, grid_dimensions): """ Get a list of absolute coordinates for the cell neighbors. @@ -43,31 +48,42 @@ class Neighborhood: :param grid_dimensions: The dimensions of the grid, to apply the edge the rule. :return: list of absolute coordinates for the cells neighbors. """ - self.__grid_dimensions = grid_dimensions - return list(self._neighbors_generator(cell_coordinate)) + self.__lazy_initialize_relative_neighborhood(grid_dimensions) + return tuple(self._neighbors_generator(cell_coordinate)) - def get_id_of_neighbor_from_relative_coordinate(self, rel_coordinate): - return self._rel_neighbors.index(rel_coordinate) + def __lazy_initialize_relative_neighborhood(self, grid_dimensions): + self._grid_dimensions = grid_dimensions + if self._rel_neighbors is None: + self._create_relative_neighborhood() + + def _create_relative_neighborhood(self): + self._rel_neighbors = tuple(self._neighborhood_generator()) + + def _neighborhood_generator(self): + for coordinate in itertools.product(range(-self._radius, self._radius + 1), repeat=len(self._grid_dimensions)): + if self._neighbor_rule(coordinate) and coordinate != (0, ) * len(self._grid_dimensions): + yield tuple(reversed(coordinate)) + + def _neighbor_rule(self, rel_neighbor): # pylint: disable=no-self-use, unused-argument + return True + + def get_neighbor_by_relative_coordinate(self, neighbors, rel_coordinate): + return neighbors[self._rel_neighbors.index(rel_coordinate)] def _neighbors_generator(self, cell_coordinate): - if not self._does_ignore_edge_cell_rule_apply(cell_coordinate): + on_edge = self.__is_coordinate_on_an_edge(cell_coordinate) + if self.__edge_rule != EdgeRule.IGNORE_EDGE_CELLS or not on_edge: # pylint: disable=too-many-nested-blocks for rel_n in self._rel_neighbors: - yield from self._calculate_abs_neighbor_and_decide_validity(cell_coordinate, rel_n) - - def _calculate_abs_neighbor_and_decide_validity(self, cell_coordinate, rel_n): - n = list(map(operator.add, rel_n, cell_coordinate)) - n_folded = self.__apply_edge_overflow(n) - if n == n_folded or self.__edge_rule == EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS: - yield n_folded - - def _does_ignore_edge_cell_rule_apply(self, coordinate): - return self.__edge_rule == EdgeRule.IGNORE_EDGE_CELLS and self.__is_coordinate_on_an_edge(coordinate) + if on_edge: + n, n_folded = zip(*[(ni + ci, (ni + di + ci) % di) + for ci, ni, di in zip(cell_coordinate, rel_n, self._grid_dimensions)]) + if self.__edge_rule == EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS or n == n_folded: + yield n_folded + else: + yield tuple(map(operator.add, rel_n, cell_coordinate)) def __is_coordinate_on_an_edge(self, coordinate): - return any(0 == ci or ci == di-1 for ci, di in zip(coordinate, self.__grid_dimensions)) - - def __apply_edge_overflow(self, n): - return list(map(lambda ni, di: (ni + di) % di, n, self.__grid_dimensions)) + return any(ci in [0, di-1] for ci, di in zip(coordinate, self._grid_dimensions)) class MooreNeighborhood(Neighborhood): @@ -86,10 +102,6 @@ class MooreNeighborhood(Neighborhood): X X X X X N N N N N """ - def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, radius=1, dimension=2): - super().__init__(tuple(_rel_neighbor_generator(dimension, radius, lambda rel_n: True)), - edge_rule) - class VonNeumannNeighborhood(Neighborhood): """ Von Neumann defined a neighborhood with a radius applied to Manhatten distance @@ -108,16 +120,11 @@ class VonNeumannNeighborhood(Neighborhood): X X X X X X X N X X """ - def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, radius=1, dimension=2): - self.radius = radius - super().__init__(tuple(_rel_neighbor_generator(dimension, radius, self.neighbor_rule)), - edge_rule) - - def neighbor_rule(self, rel_n): + def _neighbor_rule(self, rel_neighbor): cross_sum = 0 - for ci in rel_n: - cross_sum += abs(ci) - return cross_sum <= self.radius + for coordinate_i in rel_neighbor: + cross_sum += abs(coordinate_i) + return cross_sum <= self._radius class RadialNeighborhood(Neighborhood): @@ -139,18 +146,16 @@ class RadialNeighborhood(Neighborhood): X X X X X X X X X N N N X X """ - def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, radius=1, delta_=.25, dimension=2): - self.radius = radius + def __init__(self, *args, delta_=.25, **kwargs): self.delta = delta_ - super().__init__(tuple(_rel_neighbor_generator(dimension, radius, self.neighbor_rule)), - edge_rule) + super().__init__(*args, **kwargs) - def neighbor_rule(self, rel_n): + def _neighbor_rule(self, rel_neighbor): cross_sum = 0 - for ci in rel_n: - cross_sum += pow(ci, 2) + for coordinate_i in rel_neighbor: + cross_sum += pow(coordinate_i, 2) - return math.sqrt(cross_sum) <= self.radius + self.delta + return math.sqrt(cross_sum) <= self._radius + self.delta class HexagonalNeighborhood(Neighborhood): @@ -186,30 +191,27 @@ class HexagonalNeighborhood(Neighborhood): X N N N X X N N N X """ - def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, radius=1): - neighbor_lists = [[(0, 0)], - [(0, 0)]] + def __init__(self, *args, radius=1, **kwargs): + super().__init__(radius=radius, *args, **kwargs) + self.__calculate_hexagonal_neighborhood(radius) - self.__calculate_hexagonal_neighborhood(neighbor_lists, radius) + def __calculate_hexagonal_neighborhood(self, radius): + neighbor_lists = [[(0, 0)], [(0, 0)]] + for radius_i in range(1, radius + 1): + for i, neighbor in enumerate(neighbor_lists): + neighbor = _grow_neighbours(neighbor) + neighbor = self.__add_rectangular_neighbours(neighbor, radius_i, i % 2 == 1) + neighbor = sorted(neighbor, key=(lambda ne: [ne[1], ne[0]])) + neighbor.remove((0, 0)) + neighbor_lists[i] = neighbor + self._neighbor_lists = neighbor_lists - super().__init__(neighbor_lists, edge_rule) - - def __calculate_hexagonal_neighborhood(self, neighbor_lists, radius): - for r in range(1, radius + 1): - for i, n in enumerate(neighbor_lists): - n = _grow_neighbours(n) - n = self.__add_rectangular_neighbours(n, r, i % 2 == 1) - n = sorted(n, key=(lambda ne: [ne[1], ne[0]])) - n.remove((0, 0)) - neighbor_lists[i] = n - - def get_id_of_neighbor_from_relative_coordinate(self, rel_coordinate): + def get_neighbor_by_relative_coordinate(self, neighbors, rel_coordinate): # pragma: no cover raise NotImplementedError - def _neighbors_generator(self, cell_coordinate): - if not self._does_ignore_edge_cell_rule_apply(cell_coordinate): - for rel_n in self._rel_neighbors[cell_coordinate[1] % 2]: - yield from self._calculate_abs_neighbor_and_decide_validity(cell_coordinate, rel_n) + def calculate_cell_neighbor_coordinates(self, cell_coordinate, grid_dimensions): + self._rel_neighbors = self._neighbor_lists[cell_coordinate[1] % 2] + return super().calculate_cell_neighbor_coordinates(cell_coordinate, grid_dimensions) @staticmethod def __add_rectangular_neighbours(neighbours, radius, is_odd): @@ -226,12 +228,6 @@ class HexagonalNeighborhood(Neighborhood): return list(set(new_neighbours)) -def _rel_neighbor_generator(dimension, range_, rule): - for c in itertools.product(range(-range_, range_ + 1), repeat=dimension): - if rule(c) and c != (0, ) * dimension: - yield tuple(reversed(c)) - - def _grow_neighbours(neighbours): new_neighbours = neighbours[:] for n in neighbours: diff --git a/cellular_automaton/rule.py b/cellular_automaton/rule.py deleted file mode 100644 index dc1f006..0000000 --- a/cellular_automaton/rule.py +++ /dev/null @@ -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] diff --git a/cellular_automaton/state.py b/cellular_automaton/state.py deleted file mode 100644 index f3d1a85..0000000 --- a/cellular_automaton/state.py +++ /dev/null @@ -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 diff --git a/examples/conways_game_of_life.py b/examples/conways_game_of_life.py index d678489..2b63cc4 100644 --- a/examples/conways_game_of_life.py +++ b/examples/conways_game_of_life.py @@ -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) diff --git a/examples/simple_star_fall.py b/examples/simple_star_fall.py index d15bf60..3a5e8ac 100644 --- a/examples/simple_star_fall.py +++ b/examples/simple_star_fall.py @@ -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() diff --git a/examples/times.py b/examples/times.py new file mode 100644 index 0000000..09577e2 --- /dev/null +++ b/examples/times.py @@ -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)') diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..224a779 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py index 199705f..e74087f 100644 --- a/setup.py +++ b/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"] ) diff --git a/test/test_automaton.py b/test/test_automaton.py deleted file mode 100644 index f917b9e..0000000 --- a/test/test_automaton.py +++ /dev/null @@ -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() diff --git a/test/test_cell.py b/test/test_cell.py deleted file mode 100644 index 12998ae..0000000 --- a/test/test_cell.py +++ /dev/null @@ -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() diff --git a/test/test_cell_state.py b/test/test_cell_state.py deleted file mode 100644 index b8a53e2..0000000 --- a/test/test_cell_state.py +++ /dev/null @@ -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() diff --git a/test/test_display.py b/test/test_display.py deleted file mode 100644 index eb8243d..0000000 --- a/test/test_display.py +++ /dev/null @@ -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() diff --git a/test/test_factory.py b/test/test_factory.py deleted file mode 100644 index 6e379da..0000000 --- a/test/test_factory.py +++ /dev/null @@ -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() diff --git a/test/test_neighborhood.py b/test/test_neighborhood.py deleted file mode 100644 index edcda04..0000000 --- a/test/test_neighborhood.py +++ /dev/null @@ -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() diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/tests/context.py b/tests/context.py new file mode 100644 index 0000000..20a909f --- /dev/null +++ b/tests/context.py @@ -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 diff --git a/tests/test_automaton.py b/tests/test_automaton.py new file mode 100644 index 0000000..7b243d5 --- /dev/null +++ b/tests/test_automaton.py @@ -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 diff --git a/tests/test_display.py b/tests/test_display.py new file mode 100644 index 0000000..63fed3f --- /dev/null +++ b/tests/test_display.py @@ -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) diff --git a/tests/test_factory.py b/tests/test_factory.py new file mode 100644 index 0000000..5635fa5 --- /dev/null +++ b/tests/test_factory.py @@ -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)) diff --git a/tests/test_neighborhood.py b/tests/test_neighborhood.py new file mode 100644 index 0000000..8f2b979 --- /dev/null +++ b/tests/test_neighborhood.py @@ -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