From 9964e3b48334962d5867aab851ded19368cedefb Mon Sep 17 00:00:00 2001 From: Richard Feistenauer Date: Sun, 24 Feb 2019 12:37:26 +0100 Subject: [PATCH] increased api documentation --- README.md | 10 ++++++--- cellular_automaton/automaton.py | 18 +++++++++++++-- cellular_automaton/cell.py | 13 +++++++++-- cellular_automaton/cell_state.py | 13 +++++------ cellular_automaton/display.py | 5 +++-- cellular_automaton/factory.py | 4 +++- cellular_automaton/neighborhood.py | 35 +++++++++++++++++++++++++++++- cellular_automaton/rule.py | 20 +++++++++++------ cellular_automaton/state.py | 2 ++ examples/conways_game_of_life.py | 19 ++++++++++++++-- examples/simple_star_fall.py | 20 +++++++++++++++-- 11 files changed, 130 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9e34934..9bfa132 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,9 @@ class MyRule(Rule): Just inherit from `cellular_automaton.rule:Rule` and define the evolution rule and initial state. ## Visualisation -The module provides a pygame window for common two dimensional. -To add another kind of display option e.g. for other dimensions or hexagonal grids you can extrend the provided implementation or build you own. +The package provides a module for visualization in a pygame window for common two dimensional automatons. + +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 @@ -92,4 +93,7 @@ Those two example automaton implementations should provide a good start for your ## Dependencies As mentioned above the module depends on [pygame](https://www.pygame.org/news) for visualisation. -This is the only dependency however. \ No newline at end of file +This is the only dependency however. + +## Licence +This package is distributed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0), see [LICENSE](./LICENSE.txt) \ No newline at end of file diff --git a/cellular_automaton/automaton.py b/cellular_automaton/automaton.py index 7b0ea0e..8397d5b 100644 --- a/cellular_automaton/automaton.py +++ b/cellular_automaton/automaton.py @@ -21,14 +21,20 @@ from ctypes import c_int class CellularAutomatonProcessor: + """ This class is responsible for the evolution of the cells. """ + 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() 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 @@ -48,6 +54,14 @@ class CellularAutomatonProcessor: class CellularAutomatonMultiProcessor(CellularAutomatonProcessor): + """ This is a variant of CellularAutomatonProcessor that uses multi processing. + The evolution of the cells will be outsourced to new processes. + + WARNING: + This variant has high memory use! + The inter process communication overhead can make this variant slower than single processing! + """ + def __init__(self, cellular_automaton, process_count: int = 2): multiprocessing.freeze_support() if process_count < 1: @@ -57,9 +71,9 @@ class CellularAutomatonMultiProcessor(CellularAutomatonProcessor): self.evolve_range = range(len(self._ca.cells)) self._ca.current_evolution_step = RawValue(c_int, self._ca.current_evolution_step) - self.__init_processes_and_clean_cell_instances(process_count) + self.__init_processes(process_count) - def __init_processes_and_clean_cell_instances(self, process_count): + def __init_processes(self, process_count): self.pool = multiprocessing.Pool(processes=process_count, initializer=_init_process, initargs=(tuple(self._ca.cells.values()), diff --git a/cellular_automaton/cell.py b/cellular_automaton/cell.py index c610ae1..9793721 100644 --- a/cellular_automaton/cell.py +++ b/cellular_automaton/cell.py @@ -23,22 +23,31 @@ class Cell: 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_activate(new_state, evolution_step) + self.__set_new_state_and_consider_activation(new_state, evolution_step) - def set_new_state_and_activate(self, new_state: cell_state.CellState, 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: diff --git a/cellular_automaton/cell_state.py b/cellular_automaton/cell_state.py index ad3e34c..db6e707 100644 --- a/cellular_automaton/cell_state.py +++ b/cellular_automaton/cell_state.py @@ -35,20 +35,19 @@ class CellState: 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 state is set active for this evolution_step. + :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. - :return: """ 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 redraw is needed. + :return: True if state changed since last call of 'was_redrawn'. """ return self._dirty @@ -62,7 +61,7 @@ class CellState: 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 this evolution_step. + :return The state of the requested evolution_step. """ return self._state_slots[self._calculate_slot(evolution_step)] @@ -71,6 +70,7 @@ class CellState: :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 @@ -111,9 +111,8 @@ class CellState: class SynchronousCellState(CellState): - """ - CellState version using shared values for multi processing purpose. - """ + """ 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)] diff --git a/cellular_automaton/display.py b/cellular_automaton/display.py index c6a147e..11e683e 100644 --- a/cellular_automaton/display.py +++ b/cellular_automaton/display.py @@ -33,10 +33,11 @@ class _CASurface: return [self.__rect.width / grid_dimension[0], self.__rect.height / grid_dimension[1]] def redraw_cellular_automaton(self): - update_rectangles = list(self.__cell_redraw_dirty_rectangles()) + """ Redraws those cells which changed their state since last redraw. """ + update_rectangles = list(self.__redraw_dirty_cells()) pygame.display.update(update_rectangles) - def __cell_redraw_dirty_rectangles(self): + 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) diff --git a/cellular_automaton/factory.py b/cellular_automaton/factory.py index d0620e6..079504b 100644 --- a/cellular_automaton/factory.py +++ b/cellular_automaton/factory.py @@ -26,6 +26,8 @@ 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, @@ -67,7 +69,7 @@ class CAFactory: 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] + 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 0c064eb..ca99f54 100644 --- a/cellular_automaton/neighborhood.py +++ b/cellular_automaton/neighborhood.py @@ -45,7 +45,7 @@ class Neighborhood: self.__grid_dimensions = grid_dimensions return list(self.__neighbors_generator(cell_coordinate)) - def get_neighbor_id_from_rel(self, rel_coordinate): + def get_id_of_neighbor_from_relative_coordinate(self, rel_coordinate): return self._rel_neighbors.index(rel_coordinate) def __neighbors_generator(self, cell_coordinate): @@ -70,12 +70,45 @@ class Neighborhood: class MooreNeighborhood(Neighborhood): + """ Defines a Moore neighborhood: + Moore defined a neighborhood with a radius applied on a the non euclidean distance to other cells in the grid. + Example: + Moor neighborhood in 2 dimensions with radius 1 and 2 + C = cell of interest + N = neighbour of cell + X = no neighbour of cell + + Radius 1 Radius 2 + X X X X X N N N N N + X N N N X N N N N N + X N C N X N N C N N + X N N N X N N N N N + X X X X X N N N N N + """ + def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, range_=1, dimension=2): super().__init__(tuple(_rel_neighbor_generator(dimension, range_, lambda rel_n: True)), edge_rule) class VonNeumannNeighborhood(Neighborhood): + """ Defines a Von Neumann neighborhood: + Von Neumann defined a neighborhood with a radius applied to Manhatten distance + (steps between cells without diagonal movement). + Example: + Von Neumann neighborhood in 2 dimensions with radius 1 and 2 + C = cell of interest + N = neighbour of cell + X = no neighbour of cell + + Radius 1 Radius 2 + X X X X X X X N X X + X X N X X X N N N X + X N C N X N N C N N + X X N X X X N N N X + X X X X X X X N X X + """ + def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, range_=1, dimension=2): self.range_ = range_ super().__init__(tuple(_rel_neighbor_generator(dimension, range_, self.neighbor_rule)), diff --git a/cellular_automaton/rule.py b/cellular_automaton/rule.py index ea41100..dc1f006 100644 --- a/cellular_automaton/rule.py +++ b/cellular_automaton/rule.py @@ -20,30 +20,36 @@ 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_neighbor_id_from_rel(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'. - :param last_cell_state: The cells current state to calculate new state for. - :param neighbors_last_states: The cells neighbors current states. - :return: New state. - A cells evolution will only be called if it or at least one of its neighbors has changed last evolution_step cycle. + 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): - """ Set the initial state for the cell with the given coordinate. + """ Will be called to initialize a cells state. :param cell_coordinate: Cells coordinate. - :return: Iterable that represents the state + :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 index d2de334..f3d1a85 100644 --- a/cellular_automaton/state.py +++ b/cellular_automaton/state.py @@ -18,6 +18,8 @@ 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 diff --git a/examples/conways_game_of_life.py b/examples/conways_game_of_life.py index d035e55..d678489 100644 --- a/examples/conways_game_of_life.py +++ b/examples/conways_game_of_life.py @@ -1,4 +1,19 @@ #!/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. +""" import random from cellular_automaton import * @@ -8,7 +23,7 @@ ALIVE = [1.0] DEAD = [0] -class TestRule(Rule): +class ConwaysRule(Rule): random_seed = random.seed(13) def init_state(self, cell_coordinate): @@ -45,6 +60,6 @@ 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=TestRule, + rule=ConwaysRule, 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 bdcf8c2..d15bf60 100644 --- a/examples/simple_star_fall.py +++ b/examples/simple_star_fall.py @@ -1,10 +1,26 @@ #!/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. +""" import random from cellular_automaton import * -class TestRule(Rule): +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) def init_state(self, cell_coordinate): @@ -23,5 +39,5 @@ 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) + rule=StarfallRule) ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1)