increased api documentation

This commit is contained in:
Richard Feistenauer 2019-02-24 12:37:26 +01:00
parent 4507327bd5
commit 9964e3b483
11 changed files with 130 additions and 29 deletions

View File

@ -79,8 +79,9 @@ class MyRule(Rule):
Just inherit from `cellular_automaton.rule:Rule` and define the evolution rule and initial state. Just inherit from `cellular_automaton.rule:Rule` and define the evolution rule and initial state.
## Visualisation ## Visualisation
The module provides a pygame window for common two dimensional. 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 you own.
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. The visual part of this module is fully decoupled and thus should be easily replaceable.
## Examples ## Examples
@ -93,3 +94,6 @@ Those two example automaton implementations should provide a good start for your
## Dependencies ## Dependencies
As mentioned above the module depends on [pygame](https://www.pygame.org/news) for visualisation. As mentioned above the module depends on [pygame](https://www.pygame.org/news) for visualisation.
This is the only dependency however. 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)

View File

@ -21,14 +21,20 @@ from ctypes import c_int
class CellularAutomatonProcessor: class CellularAutomatonProcessor:
""" This class is responsible for the evolution of the cells. """
def __init__(self, cellular_automaton): def __init__(self, cellular_automaton):
self._ca = cellular_automaton self._ca = cellular_automaton
def evolve_x_times(self, x): 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): for x in range(x):
self.evolve() self.evolve()
def evolve(self): def evolve(self):
""" Evolve all cells """
self._ca.current_evolution_step += 1 self._ca.current_evolution_step += 1
i = self._ca.current_evolution_step i = self._ca.current_evolution_step
r = self._ca.evolution_rule.evolve_cell r = self._ca.evolution_rule.evolve_cell
@ -48,6 +54,14 @@ class CellularAutomatonProcessor:
class CellularAutomatonMultiProcessor(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): def __init__(self, cellular_automaton, process_count: int = 2):
multiprocessing.freeze_support() multiprocessing.freeze_support()
if process_count < 1: if process_count < 1:
@ -57,9 +71,9 @@ class CellularAutomatonMultiProcessor(CellularAutomatonProcessor):
self.evolve_range = range(len(self._ca.cells)) self.evolve_range = range(len(self._ca.cells))
self._ca.current_evolution_step = RawValue(c_int, self._ca.current_evolution_step) 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, self.pool = multiprocessing.Pool(processes=process_count,
initializer=_init_process, initializer=_init_process,
initargs=(tuple(self._ca.cells.values()), initargs=(tuple(self._ca.cells.values()),

View File

@ -23,22 +23,31 @@ class Cell:
self._neighbor_states = neighbors self._neighbor_states = neighbors
def is_set_for_redraw(self): 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() return self._state.is_set_for_redraw()
def was_redrawn(self): def was_redrawn(self):
""" Should be called after this cell was drawn to prevent unnecessary redraws. """
self._state.was_redrawn() self._state.was_redrawn()
def get_current_state(self, evolution_step): def get_current_state(self, evolution_step):
return self._state.get_state_of_evolution_step(evolution_step) return self._state.get_state_of_evolution_step(evolution_step)
def evolve_if_ready(self, rule, 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): if self._state.is_active(evolution_step):
new_state = rule(list(self._state.get_state_of_last_evolution_step(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]) [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) 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: if changed:
self._state.set_active_for_next_evolution_step(evolution_step) self._state.set_active_for_next_evolution_step(evolution_step)
for n in self._neighbor_states: for n in self._neighbor_states:

View File

@ -35,20 +35,19 @@ class CellState:
def is_active(self, current_evolution_step): def is_active(self, current_evolution_step):
""" Returns the active status for the requested evolution_step """ Returns the active status for the requested evolution_step
:param current_evolution_step: The evolution_step of interest. :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)] return self._active[self._calculate_slot(current_evolution_step)]
def set_active_for_next_evolution_step(self, 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. """ Sets the cell active for the next evolution_step, so it will be evolved.
:param current_evolution_step: The current evolution_step index. :param current_evolution_step: The current evolution_step index.
:return:
""" """
self._active[self._calculate_slot(current_evolution_step + 1)] = True self._active[self._calculate_slot(current_evolution_step + 1)] = True
def is_set_for_redraw(self): def is_set_for_redraw(self):
""" States if this state should be redrawn. """ 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 return self._dirty
@ -62,7 +61,7 @@ class CellState:
def get_state_of_evolution_step(self, evolution_step): def get_state_of_evolution_step(self, evolution_step):
""" Returns the state of the evolution_step. """ Returns the state of the evolution_step.
:param evolution_step: Uses the evolution_step index, to differ between concurrent states. :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)] return self._state_slots[self._calculate_slot(evolution_step)]
@ -71,6 +70,7 @@ class CellState:
:param new_state: The new state to set. :param new_state: The new state to set.
:param evolution_step: The evolution_step index, to differ between concurrent states. :param evolution_step: The evolution_step index, to differ between concurrent states.
:return True if the state really changed. :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) changed = self._set_new_state_if_valid(new_state, evolution_step)
self._dirty |= changed self._dirty |= changed
@ -111,9 +111,8 @@ class CellState:
class SynchronousCellState(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): def __init__(self, initial_state=(0., ), draw_first_state=True):
super().__init__(initial_state, draw_first_state) 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._state_slots = [RawArray(c_float, initial_state) for i in range(self.__class__._state_save_slot_count)]

View File

@ -33,10 +33,11 @@ class _CASurface:
return [self.__rect.width / grid_dimension[0], self.__rect.height / grid_dimension[1]] return [self.__rect.width / grid_dimension[0], self.__rect.height / grid_dimension[1]]
def redraw_cellular_automaton(self): 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) 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(): for coordinate, cell in self._cellular_automaton.get_cells().items():
if cell.is_set_for_redraw(): if cell.is_set_for_redraw():
yield from self.__redraw_cell(cell, coordinate) yield from self.__redraw_cell(cell, coordinate)

View File

@ -26,6 +26,8 @@ from .cell_state import CellState, SynchronousCellState
class CAFactory: class CAFactory:
""" This factory provides an easy way to create cellular automatons with single or multi processing. """
@staticmethod @staticmethod
def make_single_process_cellular_automaton(dimension, def make_single_process_cellular_automaton(dimension,
neighborhood: Neighborhood, neighborhood: Neighborhood,
@ -67,7 +69,7 @@ class CAFactory:
cells = {} cells = {}
for coordinate, cell_state in cell_states.items(): for coordinate, cell_state in cell_states.items():
n_coordinates = neighborhood.calculate_cell_neighbor_coordinates(coordinate, dimension) 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) cells[coordinate] = Cell(cell_state, neighbor_states)
return cells return cells

View File

@ -45,7 +45,7 @@ class Neighborhood:
self.__grid_dimensions = grid_dimensions self.__grid_dimensions = grid_dimensions
return list(self.__neighbors_generator(cell_coordinate)) 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) return self._rel_neighbors.index(rel_coordinate)
def __neighbors_generator(self, cell_coordinate): def __neighbors_generator(self, cell_coordinate):
@ -70,12 +70,45 @@ class Neighborhood:
class MooreNeighborhood(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): 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)), super().__init__(tuple(_rel_neighbor_generator(dimension, range_, lambda rel_n: True)),
edge_rule) edge_rule)
class VonNeumannNeighborhood(Neighborhood): 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): def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS, range_=1, dimension=2):
self.range_ = range_ self.range_ = range_
super().__init__(tuple(_rel_neighbor_generator(dimension, range_, self.neighbor_rule)), super().__init__(tuple(_rel_neighbor_generator(dimension, range_, self.neighbor_rule)),

View File

@ -20,30 +20,36 @@ from . import neighborhood
class Rule: 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): def __init__(self, neighborhood_: neighborhood.Neighborhood):
self._neighborhood = neighborhood_ self._neighborhood = neighborhood_
def _get_neighbor_by_relative_coordinate(self, neighbours, rel_coordinate): 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 @abc.abstractmethod
def evolve_cell(self, last_cell_state, neighbors_last_states): def evolve_cell(self, last_cell_state, neighbors_last_states):
""" Calculates and sets new state of 'cell'. """ Calculates and sets new state of 'cell'.
:param last_cell_state: The cells current state to calculate new state for. 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. :param neighbors_last_states: The cells neighbors current states.
:return: New state. :return: New state. The state after this evolution step
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 return last_cell_state
@abc.abstractmethod @abc.abstractmethod
def init_state(self, cell_coordinate): 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. :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] return [0]
@abc.abstractmethod @abc.abstractmethod
def get_state_draw_color(self, current_state): def get_state_draw_color(self, current_state):
""" Return the draw color for the current state """
return [0, 0, 0] return [0, 0, 0]

View File

@ -18,6 +18,8 @@ from . import Rule
class CellularAutomatonState: class CellularAutomatonState:
""" Holds all relevant information about the cellular automaton """
def __init__(self, cells, dimension, evolution_rule: Rule): def __init__(self, cells, dimension, evolution_rule: Rule):
self.cells = cells self.cells = cells
self.dimension = dimension self.dimension = dimension

View File

@ -1,4 +1,19 @@
#!/usr/bin/env python3 #!/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 import random
from cellular_automaton import * from cellular_automaton import *
@ -8,7 +23,7 @@ ALIVE = [1.0]
DEAD = [0] DEAD = [0]
class TestRule(Rule): class ConwaysRule(Rule):
random_seed = random.seed(13) random_seed = random.seed(13)
def init_state(self, cell_coordinate): def init_state(self, cell_coordinate):
@ -45,6 +60,6 @@ if __name__ == "__main__":
neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS) neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
ca = CAFactory.make_multi_process_cellular_automaton(dimension=[100, 100], ca = CAFactory.make_multi_process_cellular_automaton(dimension=[100, 100],
neighborhood=neighborhood, neighborhood=neighborhood,
rule=TestRule, rule=ConwaysRule,
processes=4) processes=4)
ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1) ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1)

View File

@ -1,10 +1,26 @@
#!/usr/bin/env python3 #!/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 import random
from cellular_automaton import * 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) random_seed = random.seed(1000)
def init_state(self, cell_coordinate): def init_state(self, cell_coordinate):
@ -23,5 +39,5 @@ if __name__ == "__main__":
neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS) neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS)
ca = CAFactory.make_single_process_cellular_automaton(dimension=[100, 100], ca = CAFactory.make_single_process_cellular_automaton(dimension=[100, 100],
neighborhood=neighborhood, neighborhood=neighborhood,
rule=TestRule) rule=StarfallRule)
ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1) ca_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1)