working fast cellular automaton

This commit is contained in:
Richard Feistenauer 2018-12-02 17:45:08 +01:00
parent 390c95df30
commit bad67211ec
6 changed files with 200 additions and 83 deletions

View File

@ -2,64 +2,96 @@
import pygame import pygame
import random import random
import time
from cellular_automaton.cellular_automaton import CellularAutomaton from cellular_automaton.cellular_automaton import CellularAutomaton
from cellular_automaton.ca_cell import CACell from cellular_automaton.ca_rule import Rule
from cellular_automaton.ca_grid import CAGrid from cellular_automaton.ca_neighborhood import MooreNeighborhood, EdgeRule
from cellular_automaton.ca_rule import CARule
class WorldGeneratorWindow: class WorldGeneratorWindow:
def __init__(self, windows_size): def __init__(self, windows_size: list, cellular_automaton: CellularAutomaton):
self.window_size = windows_size self.window_size = windows_size
self.grid_size = self.window_size.copy()
self.grid_size[1] -= 20
pygame.init() pygame.init()
pygame.display.set_caption("World Generator") pygame.display.set_caption("World Generator")
pygame.display.set_mode(self.window_size) self.screen = pygame.display.set_mode(self.grid_size)
def display_cellular_automaton(self, cellular_automaton_instance): self._cellular_automaton = cellular_automaton
pass self.font = pygame.font.SysFont("monospace", 15)
def set_cellular_automaton(self, cellular_automaton):
self._cellular_automaton = cellular_automaton
def _display_cellular_automaton(self):
grid_dimension = self._cellular_automaton.grid.get_dimension()
cell_size = [x / y for x, y in zip(self.grid_size, grid_dimension)]
surfaces_to_update = []
for cell in self._cellular_automaton.grid.get_active_cells().values():
if not cell.dirty:
continue
cell_coordinate = cell.coordinate
status = cell.get_status_for_iteration(self._cellular_automaton.get_iteration_index())
if status is None:
status = [0]
red = 0
if status[0] >= 10:
red = 255
cell_color = [red, 0, 0]
surface_pos = [x * y for x, y in zip(cell_size, cell_coordinate)]
surface_pos[1] += 20
surfaces_to_update.append(self.screen.fill(cell_color, (surface_pos, cell_size)))
pygame.display.update(surfaces_to_update)
def main_loop(self):
running = True
while running:
time_ca_start = time.time()
self._cellular_automaton.evolve()
time_ca_end = time.time()
self._display_cellular_automaton()
time_ds_end = time.time()
self._print_process_duration(time_ca_end, time_ca_start, time_ds_end)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
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")
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(): class TestRule(Rule):
def evolve_cell(self, cell, neighbors, iteration_index):
running = True last_iteration = iteration_index - 1
pygame.init() if cell.get_status_for_iteration(last_iteration) is None:
pygame.display.set_caption("minimal program") rand = random.randrange(0, 101, 1)
if rand <= 99:
screen = pygame.display.set_mode((1000, 730)) rand = 0
image = pygame.image.load("../images/map.png") cell.set_status_for_iteration([rand], iteration_index)
screen.blit(image, (0, 0)) cell.set_status_for_iteration([rand], iteration_index + 1)
screen.set_at((50, 60), [50, 0, 0]) if rand != 0:
screen.set_at((50, 61), [50, 0, 0]) cell.dirty = True
screen.set_at((51, 60), [50, 0, 0]) elif len(neighbors) == 8:
screen.set_at((51, 61), [50, 0, 0]) left_neighbour_status = neighbors[3].get_status_for_iteration(last_iteration)
pygame.display.flip() cell.set_status_for_iteration(left_neighbour_status, iteration_index)
return cell.dirty
while running:
for x in range(0, 1000):
for y in range(0, 700):
screen.set_at((x, y), [random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255)])
pygame.display.flip()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
class TestRule(CARule):
def evolve_cell(self, cell, neighbors):
if neighbors[1][0] != 0:
return [1]
else:
return [0]
if __name__ == "__main__": if __name__ == "__main__":
main()
dim = [200, 500]
ca = CellularAutomaton(2)
new_grid = CAGrid(dim)
new_grid.set_cell_by_coordinate([1, 1], CACell([1]))
rule = TestRule() rule = TestRule()
ca = CellularAutomaton([500, 500], MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS), rule, thread_count=1)
ca_window = WorldGeneratorWindow([1000, 730], ca)
ca_window.main_loop()

View File

@ -1,7 +1,26 @@
class Cell: class Cell:
def __init__(self, name: str): def __init__(self, name: str, coordinate: list):
self.name = name self.name = name
self.coordinate = coordinate
self.neighbours = [] self.neighbours = []
self._status = [None, None]
self.dirty = False
def set_neighbours(self, neighbours: list): def set_neighbours(self, neighbours: list):
self.neighbours = neighbours self.neighbours = neighbours
def set_status_for_iteration(self, new_status, iteration):
""" Will set the new status for Iteration.
:param new_status: The new status to set.
:param iteration: Uses the iteration index, to differ between current and next state.
"""
self._status[iteration % 2] = new_status
self.dirty = self._status[0] != self._status[1]
def get_status_for_iteration(self, iteration):
""" Will return the status for the iteration.
:param iteration: Uses the iteration index, to differ between current and next state.
:return The status for this iteration.
"""
return self._status[iteration % 2]

View File

@ -1,9 +1,11 @@
import sys
from cellular_automaton.ca_cell import Cell from cellular_automaton.ca_cell import Cell
from cellular_automaton.ca_neighborhood import CellularAutomatonNeighborhood from cellular_automaton.ca_neighborhood import Neighborhood
class Grid: class Grid:
def __init__(self, dimension: list, neighborhood: CellularAutomatonNeighborhood): def __init__(self, dimension: list, neighborhood: Neighborhood):
self._dimension = dimension self._dimension = dimension
self._cells = {} self._cells = {}
self._neighborhood = neighborhood self._neighborhood = neighborhood
@ -11,15 +13,22 @@ class Grid:
self._create_cells() self._create_cells()
self._set_cell_neighbours() self._set_cell_neighbours()
self._active_cells = {} self._active_cells = self._cells.copy()
self.set_all_cells_active() self._set_all_cells_active()
def set_all_cells_active(self): def get_names_of_active_cells(self):
for cell_key in self._cells: return list(self._active_cells.keys())
self._active_cells[cell_key] = 1
def get_active_cells(self): def get_active_cells(self):
return self._active_cells.keys() return self._active_cells
def clear_active_cells(self):
self._active_cells.clear()
def set_cell_and_neighbours_active(self, cell_info: list):
self._active_cells[cell_info[0].name] = cell_info[0]
for neighbour in cell_info[1]:
self._active_cells[neighbour.name] = neighbour
def get_cell_and_neighbors(self, cell_name): def get_cell_and_neighbors(self, cell_name):
cell = self._cells[cell_name] cell = self._cells[cell_name]
@ -30,6 +39,15 @@ class Grid:
return [cell, neighbour_objects] return [cell, neighbour_objects]
def get_cell_by_coordinate(self, coordinate):
return self._cells[_join_coordinate(coordinate)]
def get_dimension(self):
return self._dimension
def _set_all_cells_active(self):
for cell_key, cell in self._cells.items():
self._active_cells[cell_key] = cell
def _create_cells(self, dimension_index=0, coordinate=None): def _create_cells(self, dimension_index=0, coordinate=None):
""" Recursively steps down the dimensions to create cells in n dimensions and adds them to a dict. """ Recursively steps down the dimensions to create cells in n dimensions and adds them to a dict.
@ -37,13 +55,13 @@ class Grid:
:param coordinate: The coordinate generated so far. :param coordinate: The coordinate generated so far.
(each recursion adds one dimension to the coordinate. (each recursion adds one dimension to the coordinate.
""" """
coordinate = self.instantiate_coordinate_if_necessary(coordinate) coordinate = _instantiate_coordinate_if_necessary(coordinate)
try: try:
self._recursive_step_down_dimensions(coordinate, dimension_index, self._create_cells) self._recursive_step_down_dimensions(coordinate, dimension_index, self._create_cells)
except IndexError: except IndexError:
coordinate_string = '-'.join(coordinate) coordinate_string = _join_coordinate(coordinate)
self._cells[coordinate_string] = Cell(coordinate_string) self._cells[coordinate_string] = Cell(coordinate_string, coordinate)
def _recursive_step_down_dimensions(self, coordinate, dimension_index, recursion_method): def _recursive_step_down_dimensions(self, coordinate, dimension_index, recursion_method):
""" For the range of the current dimension, recalls the recursion method. """ For the range of the current dimension, recalls the recursion method.
@ -52,14 +70,8 @@ class Grid:
:param recursion_method: The method to call for recursion. :param recursion_method: The method to call for recursion.
""" """
for cell_index in range(self._dimension[dimension_index]): for cell_index in range(self._dimension[dimension_index]):
coordinate.append(cell_index) new_cod = coordinate + [cell_index]
recursion_method(dimension_index + 1, coordinate.copy()) recursion_method(dimension_index + 1, new_cod)
@staticmethod
def instantiate_coordinate_if_necessary(coordinate):
if coordinate is None:
coordinate = []
return coordinate
def _set_cell_neighbours(self, dimension_index=0, coordinate=None): def _set_cell_neighbours(self, dimension_index=0, coordinate=None):
""" Recursively steps down the dimensions to get the string instances for each cells neighbours. """ Recursively steps down the dimensions to get the string instances for each cells neighbours.
@ -67,12 +79,30 @@ class Grid:
:param coordinate: The coordinate generated so far. :param coordinate: The coordinate generated so far.
(each recursion adds one dimension to the coordinate. (each recursion adds one dimension to the coordinate.
""" """
coordinate = self.instantiate_coordinate_if_necessary(coordinate) coordinate = _instantiate_coordinate_if_necessary(coordinate)
try: try:
self._recursive_step_down_dimensions(coordinate, dimension_index, self._set_cell_neighbours) self._recursive_step_down_dimensions(coordinate.copy(), dimension_index, self._set_cell_neighbours)
except IndexError: except IndexError:
neighbours_coordinates = self._neighborhood.get_neighbor_coordinates(coordinate, self._dimension) neighbours_coordinates = self._neighborhood.get_neighbor_coordinates(coordinate, self._dimension)
neighbour_names = [self._cells['-'.join(nc)].name for nc in neighbours_coordinates] neighbour_names = [self._cells[_join_coordinate(nc)].name for nc in neighbours_coordinates]
self._cells['-'.join(coordinate)].set_neighbours(neighbour_names) self._cells[_join_coordinate(coordinate)].set_neighbours(neighbour_names)
# def __sizeof__(self):
# size = 0
# for cell in self._cells.values():
# size += sys.getsizeof(cell)
# size += sys.getsizeof(self._dimension) + sys.getsizeof(self._cells) + sys.getsizeof(self._active_cells)
# return size
def _instantiate_coordinate_if_necessary(coordinate):
if coordinate is None:
coordinate = []
return coordinate
def _join_coordinate(coordinate):
return '-'.join(str(x) for x in coordinate)

View File

@ -7,7 +7,7 @@ class EdgeRule(Enum):
FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS = 2 FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS = 2
class CellularAutomatonNeighborhood: class Neighborhood:
def __init__(self, neighbors: list, edge_rule: EdgeRule): def __init__(self, neighbors: list, edge_rule: EdgeRule):
self._neighbors = neighbors self._neighbors = neighbors
self.edge_rule = edge_rule self.edge_rule = edge_rule
@ -18,7 +18,9 @@ class CellularAutomatonNeighborhood:
def get_neighbor_coordinates(self, cell_coordinate, dimensions): def get_neighbor_coordinates(self, cell_coordinate, dimensions):
self.dimensions = dimensions self.dimensions = dimensions
if not self._does_ignore_edge_cell_rule_apply(cell_coordinate): if self._does_ignore_edge_cell_rule_apply(cell_coordinate):
return []
else:
return self._apply_edge_rule_to_neighbours_of(cell_coordinate) return self._apply_edge_rule_to_neighbours_of(cell_coordinate)
def _does_ignore_edge_cell_rule_apply(self, coordinate): def _does_ignore_edge_cell_rule_apply(self, coordinate):
@ -37,6 +39,7 @@ class CellularAutomatonNeighborhood:
for neighbour in self._neighbors: for neighbour in self._neighbors:
if not self._does_ignore_edge_cell_neighbours_rule_apply(neighbour, cell_coordinate): if not self._does_ignore_edge_cell_neighbours_rule_apply(neighbour, cell_coordinate):
remaining_neighbours.append(self._calculate_neighbour_coordinate(neighbour, cell_coordinate)) remaining_neighbours.append(self._calculate_neighbour_coordinate(neighbour, cell_coordinate))
return remaining_neighbours
def _does_ignore_edge_cell_neighbours_rule_apply(self, neighbour, cell_coordinate): def _does_ignore_edge_cell_neighbours_rule_apply(self, neighbour, cell_coordinate):
if self.edge_rule == EdgeRule.IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS: if self.edge_rule == EdgeRule.IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS:
@ -47,18 +50,20 @@ class CellularAutomatonNeighborhood:
return False return False
def _calculate_neighbour_coordinate(self, neighbour, cell_coordinate): def _calculate_neighbour_coordinate(self, neighbour, cell_coordinate):
new_coordinate = []
for rel_nd, cd, d in zip(neighbour, cell_coordinate, self.dimensions): for rel_nd, cd, d in zip(neighbour, cell_coordinate, self.dimensions):
nd = cd + rel_nd nd = cd + rel_nd
if nd < 0: if nd < 0:
nd = d - 1 nd = d - 1
elif nd >= d: elif nd >= d:
nd = 0 nd = 0
return nd new_coordinate.append(nd)
return new_coordinate
class MooreNeighborhood(CellularAutomatonNeighborhood): class MooreNeighborhood(Neighborhood):
def __init__(self, edge_rule: EdgeRule): def __init__(self, edge_rule: EdgeRule):
super().__init__([[-1, -1], [0, -1], [1, -1], super().__init__([[-1, -1], [0, -1], [1, -1],
[-1, 0], [0, 0], [1, 0], [-1, 0], [1, 0],
[-1, 1], [0, 1], [1, 1]], [-1, 1], [0, 1], [1, 1]],
edge_rule) edge_rule)

View File

@ -1,9 +1,18 @@
from cellular_automaton.ca_cell import Cell from cellular_automaton.ca_cell import Cell
from abc import abstractmethod
class Rule: class Rule:
def __init__(self): def __init__(self):
pass pass
def evolve_cell(self, cell: Cell, neighbours: list): @abstractmethod
pass def evolve_cell(self, cell: Cell, neighbours: list, iteration_index: int):
""" Calculates and sets new state of 'cell'.
:param cell: The cell to calculate new state for.
:param neighbours: The neighbour cells of this cell.
:param iteration_index: The current iteration index, to choose the correct state.
:return: True if state changed, False if not.
A cells evolution will only be called if it or at least one of its neighbours has changed last iteration cycle.
"""
return False

View File

@ -3,13 +3,15 @@ import time
from cellular_automaton.ca_grid import Grid from cellular_automaton.ca_grid import Grid
from cellular_automaton.ca_rule import Rule from cellular_automaton.ca_rule import Rule
from cellular_automaton.ca_neighborhood import Neighborhood
class CellularAutomaton: class CellularAutomaton:
def __init__(self, dimension: list, rule_: Rule=None, thread_count: int=4): def __init__(self, dimension: list, neighborhood: Neighborhood, rule_: Rule=None, thread_count: int=4):
self.grid = Grid(dimension) self.grid = Grid(dimension, neighborhood)
self._rule = rule_ self._rule = rule_
self._thread_count=thread_count self._thread_count = thread_count
self._iteration = 0
def set_rule(self, rule: Rule): def set_rule(self, rule: Rule):
self._rule = rule self._rule = rule
@ -17,13 +19,28 @@ class CellularAutomaton:
def set_thread_count(self, thread_count: int): def set_thread_count(self, thread_count: int):
self._thread_count = thread_count self._thread_count = thread_count
def get_iteration_index(self):
return self._iteration
def evolve(self): def evolve(self):
if self._all_cells_are_inactive():
return True
else:
self._iteration += 1
self._delegate_evolve_to_threads()
return False
def _delegate_evolve_to_threads(self):
cell_lists_for_threats = self.create_cell_lists_for_threads() cell_lists_for_threats = self.create_cell_lists_for_threads()
self.grid.clear_active_cells()
threads = self._start_treads_to_evolve_grid(cell_lists_for_threats) threads = self._start_treads_to_evolve_grid(cell_lists_for_threats)
self._wait_for_all_threads_to_finish(threads) self._wait_for_all_threads_to_finish(threads)
def _all_cells_are_inactive(self):
return len(self.grid.get_names_of_active_cells()) == 0
def create_cell_lists_for_threads(self): def create_cell_lists_for_threads(self):
active_cells = self.grid.get_active_cells() active_cells = self.grid.get_names_of_active_cells()
cell_count_per_thread = int(len(active_cells) / self._thread_count) cell_count_per_thread = int(len(active_cells) / self._thread_count)
return self.divide_active_cells(cell_count_per_thread, active_cells) return self.divide_active_cells(cell_count_per_thread, active_cells)
@ -35,7 +52,7 @@ class CellularAutomaton:
def _start_treads_to_evolve_grid(self, cell_lists_for_threats): def _start_treads_to_evolve_grid(self, cell_lists_for_threats):
threads = [] threads = []
for t in range(self._thread_count): for t in range(self._thread_count):
new_thread = _EvolutionThread(self.grid, self._rule, cell_lists_for_threats[t]) new_thread = _EvolutionThread(self.grid, self._rule, cell_lists_for_threats[t], self._iteration)
threads.append(new_thread) threads.append(new_thread)
new_thread.start() new_thread.start()
return threads return threads
@ -50,17 +67,22 @@ class CellularAutomaton:
class _EvolutionThread(threading.Thread): class _EvolutionThread(threading.Thread):
def __init__(self, grid: Grid, rule: Rule, cell_list: list): def __init__(self, grid: Grid, rule: Rule, cell_list: list, iteration: int):
super(_EvolutionThread, self).__init__() super(_EvolutionThread, self).__init__()
self._grid = grid self._grid = grid
self._rule = rule self._rule = rule
self._cell_list = cell_list self._cell_list = cell_list
self._next_state = [] self._next_state = []
self._finished = False self._finished = False
self._iteration = iteration
def run(self): def run(self):
for cell in self._cell_list: for cell in self._cell_list:
self._rule.evolve_cell(*self._grid.get_cell_and_neighbors(cell)) cell_info = self._grid.get_cell_and_neighbors(cell)
active = self._rule.evolve_cell(cell_info[0], cell_info[1], self._iteration)
if active:
self._grid.set_cell_and_neighbours_active(cell_info)
self._finished = True self._finished = True
def get_new_cell_states(self): def get_new_cell_states(self):