fixed changed bug and added multi/single processing differenciation

This commit is contained in:
Richard Feistenauer 2019-02-15 19:33:15 +01:00
parent 726cc6394f
commit be5546c833
12 changed files with 159 additions and 130 deletions

View File

@ -31,3 +31,6 @@ SIZE: 20.2575MB # process size 53 / 75,8
TOTAL TIME: 0.1135s # evolve is static
SIZE: 20.2575MB # process size 50.8 / 76 (no more cell objects in processes)
TOTAL TIME: 0.1126s # fixed changed state
SIZE: 20.8678MB

View File

@ -7,29 +7,22 @@ from cellular_automaton import *
class TestRule(Rule):
@staticmethod
def evolve_cell(last_cell_state, last_neighbour_states):
def evolve_cell(last_cell_state, neighbours_last_states):
try:
return last_neighbour_states[0]
return neighbours_last_states[0]
except IndexError:
print("damn neighbours")
pass
return False
return last_cell_state
class MyState(CellState):
class MyState(SynchronousCellState):
def __init__(self):
rand = random.randrange(0, 101, 1)
init = 0
if rand > 99:
init = 1
super().__init__((float(init),), draw_first_state=False)
init = max(.0, float(rand - 99))
super().__init__((init,), draw_first_state=init > 0)
def get_state_draw_color(self, iteration):
red = 0
if self.get_state_of_last_iteration(iteration)[0]:
red = 255
return [red, 0, 0]
state1 = self.get_state_of_iteration(iteration)[0]
return [255 if state1 else 0, 0, 0]
def make_cellular_automaton(dimension, neighborhood, rule, state_class):
@ -43,8 +36,8 @@ if __name__ == "__main__":
random.seed(1000)
# 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 = make_cellular_automaton(dimension=[400, 400], neighborhood=neighborhood, rule=TestRule(), state_class=MyState)
ca_processor = CellularAutomatonProcessor(process_count=1, cellular_automaton=ca)
ca = make_cellular_automaton(dimension=[100, 100], neighborhood=neighborhood, rule=TestRule(), state_class=MyState)
ca_processor = CellularAutomatonMultiProcessor(cellular_automaton=ca, process_count=4)
ca_window = PyGameFor2D(window_size=[1000, 800], cellular_automaton=ca)
ca_window.main_loop(cellular_automaton_processor=ca_processor, ca_iterations_per_draw=1)

Binary file not shown.

View File

@ -44,22 +44,28 @@ class CellState:
:param iteration: Uses the iteration index, to differ between concurrent states.
:return True if state has changed.
"""
changed = self._change_state(new_state, iteration)
self._change_state_values(new_state, iteration)
changed = self._did_state_change(iteration)
self._dirty |= changed
self._active[self._calculate_slot(iteration)] = False
return changed
def _change_state(self, new_state, iteration):
def _did_state_change(self, iteration):
for a, b in zip(self._state_slots[self._calculate_slot(iteration)],
self._state_slots[self._calculate_slot(iteration - 1)]):
if a != b:
return True
return False
def _change_state_values(self, new_state, iteration):
current_state = self.get_state_of_iteration(iteration)
changed = False
if len(new_state) != len(current_state):
raise IndexError("State length may not change!")
for i, ns in enumerate(new_state):
try:
if current_state[i] != ns:
changed = True
current_state[i] = ns
except IndexError:
raise IndexError("New State length or type is invalid!")
return changed
def get_state_draw_color(self, iteration):
raise NotImplementedError
@ -70,6 +76,9 @@ class CellState:
class SynchronousCellState(CellState):
"""
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)]
@ -87,7 +96,12 @@ class SynchronousCellState(CellState):
self._dirty.value = False
def set_state_of_iteration(self, new_state, iteration):
changed = self._change_state(new_state, iteration)
self._change_state_values(new_state, iteration)
changed = self._did_state_change(iteration)
self._dirty.value |= changed
self._active[self._calculate_slot(iteration)].value = False
return changed
@classmethod
def _calculate_slot(cls, iteration):
return iteration % cls._state_save_slot_count

View File

@ -25,9 +25,17 @@ class DisplayFor2D:
self._display_info = _DisplayInfo(grid_rect[-2:], grid_rect[:2], cell_size, screen)
def redraw_cellular_automaton(self):
pygame.display.update(list(_cell_redraw_rectangles(self._cellular_automaton.cells,
0,
self._display_info)))
update_rects = list(self._cell_redraw_rectangles())
pygame.display.update(update_rects)
def _cell_redraw_rectangles(self):
for cell in self._cellular_automaton.cells:
if cell.state.is_set_for_redraw():
cell_color = cell.state.get_state_draw_color(self._cellular_automaton.evolution_iteration_index)
cell_pos = _calculate_cell_position(self._display_info.cell_size, cell)
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 _calculate_cell_display_size(self, grid_size):
grid_dimension = self._cellular_automaton.dimension
@ -89,15 +97,5 @@ class PyGameFor2D:
print("SIZE: " + "{0:.4f}".format(size / (1024 * 1024)) + "MB")
def _cell_redraw_rectangles(cells, evolution_index, display_info):
for cell in cells:
if cell.state.is_set_for_redraw():
cell_color = cell.state.get_state_draw_color(evolution_index)
cell_pos = _calculate_cell_position(display_info.cell_size, cell)
surface_pos = list(map(operator.add, cell_pos, display_info.grid_pos))
yield display_info.screen.fill(cell_color, (surface_pos, display_info.cell_size))
cell.state.was_redrawn()
def _calculate_cell_position(cell_size, cell):
return list(map(operator.mul, cell_size, cell.coordinate))

View File

@ -25,8 +25,7 @@ class CAFactory:
@staticmethod
def _apply_neighbourhood_to_cells(cells, neighborhood, dimension):
for cell in cells.values():
n_coordinates = neighborhood.calculate_cell_neighbor_coordinates(cell.coordinate,
dimension)
n_coordinates = neighborhood.calculate_cell_neighbor_coordinates(cell.coordinate, dimension)
cell.neighbours = [cells[_join_coordinate(coordinate)].state for coordinate in n_coordinates]

View File

@ -1,4 +1,5 @@
from enum import Enum
from operator import add
class EdgeRule(Enum):
@ -8,12 +9,12 @@ class EdgeRule(Enum):
class Neighborhood:
def __init__(self, neighbors: list, edge_rule: EdgeRule):
def __init__(self, neighbours_relative: list, edge_rule: EdgeRule):
""" Defines a neighborhood for cells.
:param neighbors: List of relative coordinates for the neighbors.
:param edge_rule: A EdgeRule to define, how Cells on the edge of the grid will be handled.
:param neighbours_relative: List of relative coordinates of cells neighbours.
:param edge_rule: EdgeRule to define, how cells on the edge of the grid will be handled.
"""
self._neighbors = neighbors
self._rel_neighbors = neighbours_relative
self.edge_rule = edge_rule
self.grid_dimensions = []
@ -24,56 +25,31 @@ class Neighborhood:
:return:
"""
self.grid_dimensions = grid_dimensions
if self._does_ignore_edge_cell_rule_apply(cell_coordinate):
return []
else:
return self._apply_edge_rule_to_neighbours(cell_coordinate)
return list(self._neighbours_generator(cell_coordinate))
def _neighbours_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_neighbour_and_decide_validity(cell_coordinate, rel_n)
def _calculate_abs_neighbour_and_decide_validity(self, cell_coordinate, rel_n):
n = list(map(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
def _does_ignore_edge_cell_rule_apply(self, coordinate):
if self.edge_rule == EdgeRule.IGNORE_EDGE_CELLS and self._is_coordinate_on_an_edge(coordinate):
return True
return False
return self.edge_rule == EdgeRule.IGNORE_EDGE_CELLS and self._is_coordinate_on_an_edge(coordinate)
def _is_coordinate_on_an_edge(self, coordinate):
for neighbor_dimension, dimension in zip(coordinate, self.grid_dimensions):
if neighbor_dimension == 0 or neighbor_dimension == dimension - 1:
return True
return False
return all(0 == ci or ci == di-1 for ci, di in zip(coordinate, self.grid_dimensions))
def _apply_edge_rule_to_neighbours(self, coordinate):
remaining_neighbours = []
for neighbour in self._neighbors:
if not self._does_ignore_edge_cell_neighbours_rule_apply(neighbour, coordinate):
remaining_neighbours.append(self._calculate_neighbour_coordinate(neighbour, coordinate))
return remaining_neighbours
def _does_ignore_edge_cell_neighbours_rule_apply(self, neighbour, coordinate):
if self.edge_rule == EdgeRule.IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS:
for rel_neighbour_dim, cell_dim, dim in zip(neighbour, coordinate, self.grid_dimensions):
neighbor_dimension = cell_dim + rel_neighbour_dim
if neighbor_dimension < 0 or neighbor_dimension >= dim:
return True
return False
def _calculate_neighbour_coordinate(self, neighbour, cell_coordinate):
new_coordinate = []
for rel_neighbour_dim, cell_dim, dim in zip(neighbour, cell_coordinate, self.grid_dimensions):
neighbor_dim = cell_dim + rel_neighbour_dim
neighbor_dim = self._calculate_neighbour_dimension_of_edge_cells(dim, neighbor_dim)
new_coordinate.append(neighbor_dim)
return new_coordinate
@staticmethod
def _calculate_neighbour_dimension_of_edge_cells(dim, neighbor_dim):
if neighbor_dim < 0:
neighbor_dim = dim - 1
elif neighbor_dim >= dim:
neighbor_dim = 0
return neighbor_dim
def _apply_edge_overflow(self, n):
return list(map(lambda ni, di: (ni + di) % di, n, self.grid_dimensions))
class MooreNeighborhood(Neighborhood):
def __init__(self, edge_rule: EdgeRule):
def __init__(self, edge_rule: EdgeRule = EdgeRule.IGNORE_EDGE_CELLS):
super().__init__([[-1, -1], [0, -1], [1, -1],
[-1, 0], [1, 0],
[-1, 1], [0, 1], [1, 1]],

View File

@ -7,10 +7,10 @@ class Rule:
@staticmethod
@abstractmethod
def evolve_cell(last_cell_state, last_neighbour_states):
def evolve_cell(last_cell_state, neighbours_last_states):
""" Calculates and sets new state of 'cell'.
:param last_cell_state: The cells current state to calculate new state for.
:param last_neighbour_states: The cells neighbours current states.
:param neighbours_last_states: The cells neighbours current states.
: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.
"""

View File

@ -2,6 +2,7 @@ import multiprocessing
from cellular_automaton.ca_rule import Rule
from cellular_automaton.ca_cell import Cell
from ctypes import c_int
class CellularAutomaton:
@ -9,36 +10,49 @@ class CellularAutomaton:
self.cells = cells
self.dimension = dimension
self.evolution_rule = evolution_rule
self.evolution_iteration_index = multiprocessing.RawValue('i', -1)
self.evolution_iteration_index = -1
class CellularAutomatonProcessor:
def __init__(self, cellular_automaton, process_count: int = 1):
self.ca = cellular_automaton
cells = {i: (c.state, c.neighbours) for i, c in enumerate(self.ca.cells)}
self.evolve_range = range(len(self.ca.cells))
self._evolve_method = lambda x, y: None
if process_count > 1:
self.pool = multiprocessing.Pool(processes=process_count,
initializer=_init_process,
initargs=(cells,
self.ca.evolution_rule,
self.ca.evolution_iteration_index))
self._evolve_method = self.pool.map
else:
_init_process(cells, self.ca.evolution_rule, self.ca.evolution_iteration_index)
self._evolve_method = lambda x, y: list(map(x, y))
for cell in self.ca.cells:
del cell.neighbours
def __init__(self, cellular_automaton):
self._ca = cellular_automaton
def evolve_x_times(self, x):
for x in range(x):
self.evolve()
def evolve(self):
self.ca.evolution_iteration_index.value += 1
self._evolve_method(_process_routine, self.evolve_range)
self._ca.evolution_iteration_index += 1
i = self._ca.evolution_iteration_index
r = self._ca.evolution_rule.evolve_cell
list(map(lambda c: Cell.evolve_if_ready((c.state, c.neighbours), r, i), self._ca.cells))
# print(sum(1 for c in self._ca.cells if c.state.is_set_for_redraw()))
class CellularAutomatonMultiProcessor(CellularAutomatonProcessor):
def __init__(self, cellular_automaton, process_count: int = 2):
if process_count < 1:
raise ValueError
super().__init__(cellular_automaton)
self.ca = cellular_automaton
cells = {i: (c.state, c.neighbours) for i, c in enumerate(self.ca.cells)}
self.evolve_range = range(len(self.ca.cells))
self.evolution_iteration_index = multiprocessing.RawValue(c_int, -1)
self.pool = multiprocessing.Pool(processes=process_count,
initializer=_init_process,
initargs=(cells,
self.ca.evolution_rule,
self.evolution_iteration_index))
self._evolve_method = self.pool.map
for cell in self.ca.cells:
del cell.neighbours
def evolve(self):
self.ca.evolution_iteration_index += 1
self.evolution_iteration_index.value = self.ca.evolution_iteration_index
self.pool.map(_process_routine, self.evolve_range)
global_cells = None

View File

@ -6,36 +6,51 @@ import unittest
class TestCellState(unittest.TestCase):
def setUp(self):
self.cell_state = cs.SynchronousCellState(initial_state=(0,), draw_first_state=False)
def test_get_state_with_overflow(self):
cell_state = cs.CellState(initial_state=(0,))
cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
self.assertEqual(tuple(cell_state.get_state_of_iteration(2)), (1,))
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
self.assertEqual(tuple(self.cell_state.get_state_of_iteration(2)), (1,))
def test_set_state_with_overflow(self):
cell_state = cs.CellState(initial_state=(0,))
cell_state.set_state_of_iteration(new_state=(1,), iteration=2)
self.assertEqual(tuple(cell_state.get_state_of_iteration(0)), (1,))
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=3)
self.assertEqual(tuple(self.cell_state.get_state_of_iteration(1)), (1,))
def test_set_state_does_not_effect_all_slots(self):
cell_state = cs.CellState(initial_state=(0,))
cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
self.assertEqual(tuple(cell_state.get_state_of_iteration(1)), (0,))
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
self.assertEqual(tuple(self.cell_state.get_state_of_iteration(1)), (0,))
def test_redraw_state_on_change(self):
cell_state = cs.CellState(initial_state=(0,), draw_first_state=False)
cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
self.assertTrue(cell_state.is_set_for_redraw())
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
self.assertTrue(self.cell_state.is_set_for_redraw())
def test_redraw_state_on_nochange(self):
cell_state = cs.CellState(initial_state=(0,), draw_first_state=False)
cell_state.set_state_of_iteration(new_state=(0,), iteration=0)
self.assertFalse(cell_state.is_set_for_redraw())
self.cell_state.set_state_of_iteration(new_state=(0,), iteration=0)
self.assertFalse(self.cell_state.is_set_for_redraw())
def test_active_state_after_set(self):
cell_state = cs.CellState(initial_state=(0,), draw_first_state=False)
cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
self.assertFalse(cell_state.is_active(1))
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
self.assertFalse(self.cell_state.is_active(0))
self.assertFalse(self.cell_state.is_active(1))
def test_set_active_for_next_iteration(self):
self.cell_state.set_state_of_iteration(new_state=(1,), iteration=0)
self.cell_state.set_active_for_next_iteration(0)
self.assertFalse(self.cell_state.is_active(0))
self.assertTrue(self.cell_state.is_active(1))
def test_new_state_length(self):
self.assertRaises(IndexError, self.__set_state_with_new_length)
def __set_state_with_new_length(self):
return self.cell_state.set_state_of_iteration(new_state=(1, 1), iteration=0)
def test_redraw_flag(self):
self.cell_state = cs.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())
if __name__ == '__main__':

View File

@ -45,6 +45,23 @@ class TestCAFactory(unittest.TestCase):
c = fac.make_cells([2, 2, 2], CellState)
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_neighbourhood(self):
fac = TestFac()
cells = fac.make_cells([3, 3], CellState)
fac.apply_neighbourhood(cells, MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS), [3, 3])
neighbours = self.__create_neighbour_list_of_cell('1-1', cells)
self.assertEqual(set(neighbours), set(cells['1-1'].neighbours))
@staticmethod
def __create_neighbour_list_of_cell(cell_id, cells):
neighbours = []
for c in cells.values():
if c != cells[cell_id]:
neighbours.append(c.state)
return neighbours
if __name__ == '__main__':
unittest.main()

View File

@ -11,7 +11,7 @@ class TestNeighborhood(unittest.TestCase):
for neighborhood_set in neighborhood_sets:
neighbors = neighborhood.calculate_cell_neighbor_coordinates(neighborhood_set[0], [3, 3])
if neighborhood_set[1] != neighbors:
print((neighborhood_set[1]), (neighbors))
print("Error neighbours do not fit (expected, real): ", (neighborhood_set[1]), neighbors)
return False
return True