From 5e8b07799b55c66736bf28482d55506dfc75bac0 Mon Sep 17 00:00:00 2001 From: Richard Feistenauer Date: Sat, 23 Feb 2019 16:20:48 +0100 Subject: [PATCH] refactoring and licensing --- LICENSE.txt | 174 +++++++++++++++++++++++++++++ cellular_automaton/__init__.py | 10 +- cellular_automaton/_automaton.py | 23 +++- cellular_automaton/_cell.py | 48 ++++++-- cellular_automaton/_cell_state.py | 16 +++ cellular_automaton/_state.py | 21 +++- cellular_automaton/display.py | 125 +++++++++++++-------- cellular_automaton/factory.py | 74 +++++++++--- cellular_automaton/neighborhood.py | 31 ++++- cellular_automaton/rule.py | 43 ++++++- examples/conways_game_of_life.py | 37 ++++-- examples/simple_star_fall.py | 2 +- test/test_automaton.py | 72 ++++++++++++ test/test_cell.py | 33 ++++-- test/test_cell_state.py | 26 ++++- test/test_factory.py | 99 +++++++++++----- test/test_neighborhood.py | 18 ++- 17 files changed, 694 insertions(+), 158 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index e69de29..895657b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. \ No newline at end of file diff --git a/cellular_automaton/__init__.py b/cellular_automaton/__init__.py index aa0e537..99103bf 100644 --- a/cellular_automaton/__init__.py +++ b/cellular_automaton/__init__.py @@ -1,6 +1,4 @@ -from .cell_state import * -from .neighborhood import * -from .rule import * -from .factory import * -from .automaton import * -from .display import * +from .neighborhood import Neighborhood, MooreNeighborhood, VonNeumannNeighborhood, EdgeRule +from .rule import Rule +from .factory import CAFactory +from .display import CAWindow diff --git a/cellular_automaton/_automaton.py b/cellular_automaton/_automaton.py index 85cd3fc..52362a4 100644 --- a/cellular_automaton/_automaton.py +++ b/cellular_automaton/_automaton.py @@ -1,3 +1,19 @@ +""" +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 multiprocessing from multiprocessing import freeze_support from ctypes import c_int @@ -12,10 +28,10 @@ class CellularAutomatonProcessor: self.evolve() def evolve(self): - 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()))) + self._ca.current_evolution_step += 1 def get_dimension(self): return self._ca.dimension @@ -40,7 +56,6 @@ class CellularAutomatonMultiProcessor(CellularAutomatonProcessor): self.evolve_range = range(len(self._ca.cells)) self._ca.current_evolution_step = multiprocessing.RawValue(c_int, self._ca.current_evolution_step) - self.__init_processes_and_clean_cell_instances(process_count) def __init_processes_and_clean_cell_instances(self, process_count): @@ -49,12 +64,10 @@ class CellularAutomatonMultiProcessor(CellularAutomatonProcessor): initargs=(tuple(self._ca.cells.values()), self._ca.evolution_rule, self._ca.current_evolution_step)) - for cell in self._ca.cells.values(): - del cell.neighbor_states def evolve(self): - self._ca.current_evolution_step += 1 self.pool.map(_process_routine, self.evolve_range) + self._ca.current_evolution_step.value += 1 def get_current_evolution_step(self): return self._ca.current_evolution_step.value diff --git a/cellular_automaton/_cell.py b/cellular_automaton/_cell.py index ff487b5..a260551 100644 --- a/cellular_automaton/_cell.py +++ b/cellular_automaton/_cell.py @@ -1,21 +1,45 @@ -from .cell_state import CellState -from typing import Type +""" +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 cellular_automaton.cellular_automaton._cell_state as cs class Cell: - def __init__(self, state_class: Type[CellState]): - self.state = state_class() - self.neighbor_states = [] + def __init__(self, state_class: cs.CellState, neighbors): + self._state = state_class + self._neighbor_states = neighbors + + def is_set_for_redraw(self): + return self._state.is_set_for_redraw() + + def was_redrawn(self): + 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): - if self.state.is_active(evolution_step): - new_state = rule(self.state.get_state_of_last_evolution_step(evolution_step), - [n.get_state_of_last_evolution_step(evolution_step) for n in self.neighbor_states]) + 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_activate(new_state, evolution_step) - def set_new_state_and_activate(self, new_state: CellState, evolution_step): - changed = self.state.set_state_of_evolution_step(new_state, evolution_step) + def set_new_state_and_activate(self, new_state: cs.CellState, evolution_step): + changed = self._state.set_state_of_evolution_step(new_state, evolution_step) if changed: - self.state.set_active_for_next_evolution_step(evolution_step) - for n in self.neighbor_states: + 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 index 086af70..7f71bc5 100644 --- a/cellular_automaton/_cell_state.py +++ b/cellular_automaton/_cell_state.py @@ -1,3 +1,19 @@ +""" +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 import RawArray, RawValue from ctypes import c_float, c_bool diff --git a/cellular_automaton/_state.py b/cellular_automaton/_state.py index e5792e7..0dc923e 100644 --- a/cellular_automaton/_state.py +++ b/cellular_automaton/_state.py @@ -1,10 +1,25 @@ +""" +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 cellular_automaton.cellular_automaton import Rule -from typing import Type class CellularAutomatonState: - def __init__(self, cells, dimension, evolution_rule: Type[Rule]): + def __init__(self, cells, dimension, evolution_rule: Rule): self.cells = cells self.dimension = dimension self.evolution_rule = evolution_rule - self.current_evolution_step = -1 + self.current_evolution_step = 0 diff --git a/cellular_automaton/display.py b/cellular_automaton/display.py index d3745f8..d85f351 100644 --- a/cellular_automaton/display.py +++ b/cellular_automaton/display.py @@ -1,76 +1,105 @@ +""" +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 pygame import time import operator -from . import CellularAutomatonState, CellularAutomatonProcessor +import cellular_automaton.cellular_automaton._automaton as automaton -class _DisplayInfo: - def __init__(self, grid_size, grid_pos, cell_size, screen): - self.grid_size = grid_size - self.grid_pos = grid_pos - self.cell_size = cell_size - self.screen = screen - - -class DisplayFor2D: - def __init__(self, grid_rect: list, cellular_automaton: CellularAutomatonState, screen): +class _CASurface: + def __init__(self, grid_rect: pygame.Rect, cellular_automaton: automaton.CellularAutomatonProcessor, screen): self._cellular_automaton = cellular_automaton - cell_size = self._calculate_cell_display_size(grid_rect[-2:]) - self._display_info = _DisplayInfo(grid_rect[-2:], grid_rect[:2], cell_size, screen) + self.__rect = grid_rect + self.__cell_size = self._calculate_cell_display_size() + self.__screen = screen + + 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 redraw_cellular_automaton(self): - update_rects = list(self._cell_redraw_rectangles()) + update_rects = list(self.__cell_redraw_dirty_rectangles()) pygame.display.update(update_rects) - def _cell_redraw_rectangles(self): - for coordinate, cell in self._cellular_automaton.cells.items(): - if cell.state.is_set_for_redraw(): - cell_color = cell.state.get_state_draw_color(self._cellular_automaton.current_evolution_step) - cell_pos = self._calculate_cell_position(self._display_info.cell_size, coordinate) - surface_pos = list(map(operator.add, cell_pos, self._display_info.grid_pos)) - yield self._display_info.screen.fill(cell_color, (surface_pos, self._display_info.cell_size)) - cell.state.was_redrawn() + def __cell_redraw_dirty_rectangles(self): + for coordinate, cell in self._cellular_automaton.get_cells().items(): + if cell.is_set_for_redraw(): + yield from self.__redraw_cell(cell, coordinate) - def _calculate_cell_display_size(self, grid_size): - grid_dimension = self._cellular_automaton.dimension - return list(map(operator.truediv, grid_size, grid_dimension)) + 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) + yield self._draw_the_cell_to_screen(cell_color, surface_pos) + cell.was_redrawn() - @staticmethod - def _calculate_cell_position(cell_size, coordinate): - return list(map(operator.mul, cell_size, coordinate)) + 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 _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): + return [self.__rect.left + cell_pos[0], self.__rect.top + cell_pos[1]] + + def _draw_the_cell_to_screen(self, cell_color, surface_pos): + return self.__screen.fill(cell_color, (surface_pos, self.__cell_size)) -class PyGameFor2D: - def __init__(self, window_size: list, cellular_automaton: CellularAutomatonState): - self._window_size = window_size - self._cellular_automaton = cellular_automaton +class CAWindow: + def __init__(self, cellular_automaton: automaton.CellularAutomatonProcessor, + evolution_steps_per_draw=1, + window_size=(1000, 800)): + self._ca = cellular_automaton + self.__window_size = window_size + self.__init_pygame() + self.__loop_evolution_and_redraw_of_automaton(evolution_steps_per_draw=evolution_steps_per_draw) + + def __init_pygame(self): pygame.init() pygame.display.set_caption("Cellular Automaton") - self._screen = pygame.display.set_mode(self._window_size) + self._screen = pygame.display.set_mode(self.__window_size) self._font = pygame.font.SysFont("monospace", 15) - self.ca_display = DisplayFor2D([0, 30, window_size[0], window_size[1] - 30], cellular_automaton, self._screen) + self.ca_display = _CASurface(pygame.Rect(0, 30, self.__window_size[0], self.__window_size[1] - 30), + self._ca, + self._screen) - def _print_process_duration(self, time_ca_end, time_ca_start, time_ds_end): - self._screen.fill([0, 0, 0], ((0, 0), (self._window_size[0], 30))) - self._write_text((10, 5), "CA: " + "{0:.4f}".format(time_ca_end - time_ca_start) + "s") - self._write_text((310, 5), "Display: " + "{0:.4f}".format(time_ds_end - time_ca_end) + "s") - self._write_text((660, 5), "Step: " + str(self._cellular_automaton.current_evolution_step)) - - def _write_text(self, pos, text, color=(0, 255, 0)): - label = self._font.render(text, 1, color) - update_rect = self._screen.blit(label, pos) - pygame.display.update(update_rect) - - def main_loop(self, cellular_automaton_processor: CellularAutomatonProcessor, evolution_steps_per_draw): + def __loop_evolution_and_redraw_of_automaton(self, evolution_steps_per_draw): running = True while running: pygame.event.get() time_ca_start = time.time() - cellular_automaton_processor.evolve_x_times(evolution_steps_per_draw) + 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) + 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): + self._screen.fill([0, 0, 0], ((0, 0), (self.__window_size[0], 30))) + self.__write_text((10, 5), "CA: " + "{0:.4f}".format(time_ca_end - time_ca_start) + "s") + self.__write_text((310, 5), "Display: " + "{0:.4f}".format(time_ds_end - time_ca_end) + "s") + self.__write_text((660, 5), "Step: " + str(self._ca.get_current_evolution_step())) + + def __write_text(self, pos, text, color=(0, 255, 0)): + label = self._font.render(text, 1, color) + update_rect = self._screen.blit(label, pos) + pygame.display.update(update_rect) diff --git a/cellular_automaton/factory.py b/cellular_automaton/factory.py index fdb5679..a4909f8 100644 --- a/cellular_automaton/factory.py +++ b/cellular_automaton/factory.py @@ -1,30 +1,72 @@ -from . import Neighborhood, CellState, Rule +from . import Neighborhood, Rule +from ._automaton import CellularAutomatonProcessor, CellularAutomatonMultiProcessor from ._cell import Cell from ._state import CellularAutomatonState +from ._cell_state import CellState, SynchronousCellState from typing import Type +""" +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 class CAFactory: @staticmethod - def make_cellular_automaton(dimension, - neighborhood: Neighborhood, - state_class: Type[CellState], - rule: Type[Rule]): - cells = CAFactory._make_cells(dimension, state_class) - CAFactory._apply_neighborhood_to_cells(cells, neighborhood, dimension) + 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_cells(dimension, state_class): - cells = {} - for c in itertools.product(*[range(d) for d in dimension]): - cells[tuple(c)] = Cell(state_class) - return cells + 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 _apply_neighborhood_to_cells(cells, neighborhood, dimension): - for coordinate, cell in cells.items(): - n_coordinates = neighborhood.calculate_cell_neighbor_coordinates(coordinate, dimension) - cell.neighbor_states = [cells[tuple(nc)].state for nc in n_coordinates] + 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 = [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 c023127..0c064eb 100644 --- a/cellular_automaton/neighborhood.py +++ b/cellular_automaton/neighborhood.py @@ -1,9 +1,25 @@ -from enum import Enum -from operator import add -from itertools import product +""" +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 enum +import operator +import itertools -class EdgeRule(Enum): +class EdgeRule(enum.Enum): IGNORE_EDGE_CELLS = 0 IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS = 1 FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS = 2 @@ -29,13 +45,16 @@ class Neighborhood: self.__grid_dimensions = grid_dimensions return list(self.__neighbors_generator(cell_coordinate)) + def get_neighbor_id_from_rel(self, rel_coordinate): + return self._rel_neighbors.index(rel_coordinate) + def __neighbors_generator(self, cell_coordinate): if not self.__does_ignore_edge_cell_rule_apply(cell_coordinate): 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(add, rel_n, cell_coordinate)) + 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 @@ -70,6 +89,6 @@ class VonNeumannNeighborhood(Neighborhood): def _rel_neighbor_generator(dimension, range_, rule): - for c in product(range(-range_, range_ + 1), repeat=dimension): + for c in itertools.product(range(-range_, range_ + 1), repeat=dimension): if rule(c) and c != (0, ) * dimension: yield tuple(reversed(c)) diff --git a/cellular_automaton/rule.py b/cellular_automaton/rule.py index 799b21b..9c2536c 100644 --- a/cellular_automaton/rule.py +++ b/cellular_automaton/rule.py @@ -1,13 +1,32 @@ -from abc import abstractmethod +""" +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 +import cellular_automaton.cellular_automaton.neighborhood as neighbour class Rule: - def __init__(self): - pass + def __init__(self, neighborhood_: neighbour.Neighborhood): + self._neighborhood = neighborhood_ - @staticmethod - @abstractmethod - def evolve_cell(last_cell_state, neighbors_last_states): + def _get_neighbor_by_relative_coordinate(self, neighbours, rel_coordinate): + return neighbours[self._neighborhood.get_neighbor_id_from_rel(rel_coordinate)] + + @abc.abstractmethod + def evolve_cell(self, last_cell_state, neighbors_last_states): """ Calculates and sets new state of 'cell'. :param last_cell_state: The cells current state to calculate new state for. :param neighbors_last_states: The cells neighbors current states. @@ -15,3 +34,15 @@ class Rule: A cells evolution will only be called if it or at least one of its neighbors has changed last evolution_step cycle. """ return last_cell_state + + @abc.abstractmethod + def init_state(self, cell_coordinate): + """ Set the initial state for the cell with the given coordinate. + :param cell_coordinate: Cells coordinate. + :return: Iterable that represents the state + """ + return [0] + + @abc.abstractmethod + def get_state_draw_color(self, current_state): + return [0, 0, 0] diff --git a/examples/conways_game_of_life.py b/examples/conways_game_of_life.py index 61363a5..66fad72 100644 --- a/examples/conways_game_of_life.py +++ b/examples/conways_game_of_life.py @@ -4,16 +4,38 @@ import random from cellular_automaton import * +ALIVE = [1.0] +DEAD = [0] + + class TestRule(Rule): - random_seed = random.seed(1000) + random_seed = random.seed(13) def init_state(self, cell_coordinate): - rand = random.randrange(0, 101, 1) - init = max(.0, float(rand - 99)) + rand = random.randrange(0, 16, 1) + init = max(.0, float(rand - 14)) return (init,) def evolve_cell(self, last_cell_state, neighbors_last_states): - return self._get_neighbor_by_relative_coordinate(neighbors_last_states, (-1, -1)) + new_cell_state = last_cell_state + alive_neighbours = self.__count_alive_neighbours(neighbors_last_states) + if last_cell_state == DEAD and alive_neighbours == 3: + new_cell_state = ALIVE + if last_cell_state == ALIVE and alive_neighbours < 2: + new_cell_state = DEAD + if last_cell_state == ALIVE and 1 < alive_neighbours < 4: + new_cell_state = ALIVE + if last_cell_state == ALIVE and alive_neighbours > 3: + new_cell_state = DEAD + return new_cell_state + + @staticmethod + def __count_alive_neighbours(neighbours): + an = [] + 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] @@ -21,7 +43,8 @@ class TestRule(Rule): 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=TestRule) + ca = CAFactory.make_multi_process_cellular_automaton(dimension=[100, 100], + neighborhood=neighborhood, + rule=TestRule, + processes=4) ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1) diff --git a/examples/simple_star_fall.py b/examples/simple_star_fall.py index c2c19cd..61363a5 100644 --- a/examples/simple_star_fall.py +++ b/examples/simple_star_fall.py @@ -24,4 +24,4 @@ if __name__ == "__main__": ca = CAFactory.make_single_process_cellular_automaton(dimension=[100, 100], neighborhood=neighborhood, rule=TestRule) - ca_window = PyGameFor2D(cellular_automaton=ca, evolution_steps_per_draw=1) + ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1) diff --git a/test/test_automaton.py b/test/test_automaton.py index e69de29..6ae308d 100644 --- a/test/test_automaton.py +++ b/test/test_automaton.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. +""" + +import sys +sys.path.append('../..') + +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 index 9b6d439..ec83879 100644 --- a/test/test_cell.py +++ b/test/test_cell.py @@ -1,25 +1,36 @@ +""" +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('../src') +sys.path.append('../..') -from cellular_automaton.cellular_automaton import * +from cellular_automaton.cellular_automaton._cell import Cell +from cellular_automaton.cellular_automaton._cell_state import CellState import unittest -class TestState(CellState): - def __init__(self): - super().__init__() - - class TestCellState(unittest.TestCase): def setUp(self): - self.cell = Cell(TestState) - self.neighbors = [TestState() for x in range(5)] + self.neighbors = [CellState() for x in range(5)] for neighbor in self.neighbors: neighbor.set_state_of_evolution_step((0, ), 0) - self.cell.neighbor_states = self.neighbors + self.cell = Cell(CellState(), self.neighbors) def cell_and_neighbors_active(self, evolution_step): - self.neighbors.append(self.cell.state) + self.neighbors.append(self.cell._state) all_active = True for state in self.neighbors: if not state.is_active(evolution_step): diff --git a/test/test_cell_state.py b/test/test_cell_state.py index 30f3396..0e46d5f 100644 --- a/test/test_cell_state.py +++ b/test/test_cell_state.py @@ -1,13 +1,29 @@ -import sys -sys.path.append('../src') +""" +Copyright 2019 Richard Feistenauer -from cellular_automaton import cellular_automaton as cs +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 import _cell_state as cell_state import unittest class TestCellState(unittest.TestCase): def setUp(self): - self.cell_state = cs.SynchronousCellState(initial_state=(0,), draw_first_state=False) + self.cell_state = 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) @@ -47,7 +63,7 @@ class TestCellState(unittest.TestCase): return self.cell_state.set_state_of_evolution_step(new_state=(1, 1), evolution_step=0) def test_redraw_flag(self): - self.cell_state = cs.SynchronousCellState(initial_state=(0,), draw_first_state=True) + self.cell_state = 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()) diff --git a/test/test_factory.py b/test/test_factory.py index 116e8ac..eee91cc 100644 --- a/test/test_factory.py +++ b/test/test_factory.py @@ -1,19 +1,52 @@ +""" +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('../src') +sys.path.append('../..') from cellular_automaton.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_cells(dimension, state_class): - return CAFactory._make_cells(dimension, state_class) + def make_cell_states(state_class, rule_, dimension): + return CAFactory._make_cell_states(state_class, rule_, dimension) @staticmethod - def apply_neighborhood(cells, neighborhood, dimension): - return CAFactory._apply_neighborhood_to_cells(cells, neighborhood, dimension) + 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): @@ -21,48 +54,52 @@ class TestCAFactory(unittest.TestCase): self._neighborhood = MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS) def test_make_ca_calls_correct_methods(self): - with mock.patch.object(CAFactory, '_make_cells', return_value={1: True}) as m1: - with mock.patch.object(CAFactory, '_apply_neighborhood_to_cells') as m2: - CAFactory.make_cellular_automaton([10], self._neighborhood, CellState, Rule()) - m1.assert_called_once_with([10], CellState) + 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_cells', return_value={1: True}): - with mock.patch.object(CAFactory, '_apply_neighborhood_to_cells'): - ca = CAFactory.make_cellular_automaton([10], self._neighborhood, CellState, Rule()) + 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_cells([3], CellState) + 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_cells([2, 2], CellState) + 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_cells([2, 2, 2], CellState) + 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)]) - def test_apply_neighborhood(self): - cells = TestFac.make_cells([3, 3], CellState) - TestFac.apply_neighborhood(cells, self._neighborhood, [3, 3]) - - neighbors = self.__create_neighbor_list_of_cell((1, 1), cells) - - self.assertEqual(set(neighbors), set(cells[(1, 1)].neighbor_states)) - - @staticmethod - def __create_neighbor_list_of_cell(cell_id, cells): - neighbors = [] - for c in cells.values(): - if c != cells[cell_id]: - neighbors.append(c.state) - return neighbors - if __name__ == '__main__': unittest.main() diff --git a/test/test_neighborhood.py b/test/test_neighborhood.py index 6241672..4bb31e6 100644 --- a/test/test_neighborhood.py +++ b/test/test_neighborhood.py @@ -1,5 +1,21 @@ +""" +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('../src') +sys.path.append('../..') from cellular_automaton import cellular_automaton as csn import unittest