diff --git a/README.md b/README.md index e69de29..9e34934 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,95 @@ +# Cellular Automaton +This package provides an cellular automaton for [`Python® 3`](https://www.python.org/) + +A cellular automaton defines a grid of cells and a set of rules. +All cells then evolve their state depending on their neighbours state simultaneously. + +For further information on cellular automatons consult e.g. [mathworld.wolfram.com](http://mathworld.wolfram.com/CellularAutomaton.html) + +## Yet another cellular automaton module? +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 + +I originally did not plan to write a new cellular automaton module, +but when searching for one, I just found some minimalistic implementations, +that had little or no documentation with an API that really did not fit the problem +and Code that was desperately asking for some refactoring. + +So I started to write my own module with the goal to provide an user friendly API +and acceptable documentation. During the implementation I figured, why not just provide +n dimensional support and with reading Clean Code from Robert C. Martin the urge +to have a clean and tested code with a decent coverage added some more requirements. +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. + +## Usage +To start and use the automaton you will have to define three things: +- The neighborhood +- The dimensions of the grid +- The evolution rule + +`````python +neighborhood = MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS) +ca = CAFactory.make_single_process_cellular_automaton(dimension=[100, 100], + neighborhood=neighborhood, + rule=MyRule) +`````` + +### Neighbourhood +The Neighborhood defines for a cell neighbours in relative coordinates. +The evolution of a cell will depend solely on those neighbours. + +The Edge Rule passed as parameter to the Neighborhood defines, how cells on the edge of the grid will be handled. +There are three options: +- Ignore edge cells: Edge cells will have no neighbours and thus not evolve. +- Ignore missing neighbours: Edge cells will add the neighbours that exist. This results in varying count of neighbours on edge cells. +- First and last cell of each dimension are neighbours: All cells will have the same neighbour count and no edge exists. + +### Dimension +A list or Tuple which states each dimensions size. +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. + +## 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 visual part of this module is fully decoupled and thus should be easily replaceable. + +## Examples +The package contains two examples: +- [simple_star_fall](./examples/simple_star_fall.py) +- [conways_game_of_life](./examples/conways_game_of_life.py) + +Those two example automaton implementations should provide a good start for your own automaton. + +## 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 diff --git a/cellular_automaton/automaton.py b/cellular_automaton/_automaton.py similarity index 76% rename from cellular_automaton/automaton.py rename to cellular_automaton/_automaton.py index e4e5911..85cd3fc 100644 --- a/cellular_automaton/automaton.py +++ b/cellular_automaton/_automaton.py @@ -17,6 +17,18 @@ class CellularAutomatonProcessor: r = self._ca.evolution_rule.evolve_cell list(map(lambda c: c.evolve_if_ready(r, i), tuple(self._ca.cells.values()))) + def get_dimension(self): + return self._ca.dimension + + def get_cells(self): + return self._ca.cells + + def get_current_evolution_step(self): + return self._ca.current_evolution_step + + def get_current_rule(self): + return self._ca.evolution_rule + class CellularAutomatonMultiProcessor(CellularAutomatonProcessor): def __init__(self, cellular_automaton, process_count: int = 2): @@ -27,7 +39,7 @@ class CellularAutomatonMultiProcessor(CellularAutomatonProcessor): super().__init__(cellular_automaton) self.evolve_range = range(len(self._ca.cells)) - self.shared_evolution_step = multiprocessing.RawValue(c_int, self._ca.current_evolution_step) + self._ca.current_evolution_step = multiprocessing.RawValue(c_int, self._ca.current_evolution_step) self.__init_processes_and_clean_cell_instances(process_count) @@ -36,15 +48,17 @@ class CellularAutomatonMultiProcessor(CellularAutomatonProcessor): initializer=_init_process, initargs=(tuple(self._ca.cells.values()), self._ca.evolution_rule, - self.shared_evolution_step)) + 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.shared_evolution_step.value = self._ca.current_evolution_step 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 diff --git a/cellular_automaton/cell_state.py b/cellular_automaton/_cell_state.py similarity index 100% rename from cellular_automaton/cell_state.py rename to cellular_automaton/_cell_state.py diff --git a/examples/basic_2d_ca.py b/examples/basic_2d_ca.py deleted file mode 100644 index 5344735..0000000 --- a/examples/basic_2d_ca.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 - -import random -from cellular_automaton import * - - -class TestRule(Rule): - @staticmethod - def evolve_cell(last_cell_state, neighbors_last_states): - try: - return neighbors_last_states[0] - except IndexError: - return last_cell_state - - -# class MyState(SynchronousCellState): -class MyState(CellState): - random_seed = random.seed(1000) - - def __init__(self): - rand = random.randrange(0, 101, 1) - init = max(.0, float(rand - 99)) - super().__init__((init,), draw_first_state=init > 0) - - def get_state_draw_color(self, evolution_step): - state = self.get_state_of_evolution_step(evolution_step)[0] - return [255 if state else 0, 0, 0] - - -if __name__ == "__main__": - # best single is 400/400 with 0,2 ca speed and 0,09 redraw / multi is 300/300 with 0.083 - neighborhood = MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS) - ca = CAFactory.make_cellular_automaton(dimension=[100, 100], - neighborhood=neighborhood, - rule=TestRule(), - state_class=MyState) - # ca_processor = CellularAutomatonMultiProcessor(cellular_automaton=ca, process_count=4) - ca_processor = CellularAutomatonProcessor(cellular_automaton=ca) - - ca_window = PyGameFor2D(window_size=[1000, 800], cellular_automaton=ca) - ca_window.main_loop(cellular_automaton_processor=ca_processor, evolution_steps_per_draw=1) diff --git a/examples/conways_game_of_life.py b/examples/conways_game_of_life.py new file mode 100644 index 0000000..61363a5 --- /dev/null +++ b/examples/conways_game_of_life.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +import random +from cellular_automaton import * + + +class TestRule(Rule): + random_seed = random.seed(1000) + + def init_state(self, cell_coordinate): + rand = random.randrange(0, 101, 1) + init = max(.0, float(rand - 99)) + return (init,) + + 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] + + +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_window = CAWindow(cellular_automaton=ca, evolution_steps_per_draw=1) diff --git a/examples/simple_star_fall.py b/examples/simple_star_fall.py new file mode 100644 index 0000000..c2c19cd --- /dev/null +++ b/examples/simple_star_fall.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +import random +from cellular_automaton import * + + +class TestRule(Rule): + random_seed = random.seed(1000) + + def init_state(self, cell_coordinate): + rand = random.randrange(0, 101, 1) + init = max(.0, float(rand - 99)) + return (init,) + + 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] + + +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_window = PyGameFor2D(cellular_automaton=ca, evolution_steps_per_draw=1) diff --git a/test/test_automaton.py b/test/test_automaton.py new file mode 100644 index 0000000..e69de29