diff --git a/performance.txt b/performance.txt index 277cf6d..c833315 100644 --- a/performance.txt +++ b/performance.txt @@ -30,4 +30,7 @@ TOTAL TIME: 0.1152s # dict instead of list for cells 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) \ No newline at end of file +SIZE: 20.2575MB # process size 50.8 / 76 (no more cell objects in processes) + +TOTAL TIME: 0.1126s # fixed changed state +SIZE: 20.8678MB \ No newline at end of file diff --git a/scripts/main_ui.py b/scripts/main_ui.py index 55d371a..de51922 100644 --- a/scripts/main_ui.py +++ b/scripts/main_ui.py @@ -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) diff --git a/scripts/performance_test b/scripts/performance_test index 0bc20ef..beb9614 100644 Binary files a/scripts/performance_test and b/scripts/performance_test differ diff --git a/src/cellular_automaton/ca_cell_state.py b/src/cellular_automaton/ca_cell_state.py index bc70f78..59a592a 100644 --- a/src/cellular_automaton/ca_cell_state.py +++ b/src/cellular_automaton/ca_cell_state.py @@ -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 diff --git a/src/cellular_automaton/ca_display.py b/src/cellular_automaton/ca_display.py index 9fee63e..ecd8b21 100644 --- a/src/cellular_automaton/ca_display.py +++ b/src/cellular_automaton/ca_display.py @@ -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)) diff --git a/src/cellular_automaton/ca_factory.py b/src/cellular_automaton/ca_factory.py index de998f2..e8a65a1 100644 --- a/src/cellular_automaton/ca_factory.py +++ b/src/cellular_automaton/ca_factory.py @@ -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] diff --git a/src/cellular_automaton/ca_neighborhood.py b/src/cellular_automaton/ca_neighborhood.py index 967b17c..2195edf 100644 --- a/src/cellular_automaton/ca_neighborhood.py +++ b/src/cellular_automaton/ca_neighborhood.py @@ -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]], diff --git a/src/cellular_automaton/ca_rule.py b/src/cellular_automaton/ca_rule.py index cb0bad0..2af43b9 100644 --- a/src/cellular_automaton/ca_rule.py +++ b/src/cellular_automaton/ca_rule.py @@ -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. """ diff --git a/src/cellular_automaton/cellular_automaton.py b/src/cellular_automaton/cellular_automaton.py index e745107..39c7061 100644 --- a/src/cellular_automaton/cellular_automaton.py +++ b/src/cellular_automaton/cellular_automaton.py @@ -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 diff --git a/test/test_cell_state.py b/test/test_cell_state.py index 05e03de..8927a4e 100644 --- a/test/test_cell_state.py +++ b/test/test_cell_state.py @@ -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__': diff --git a/test/test_factory.py b/test/test_factory.py index 30b8083..df5fd8e 100644 --- a/test/test_factory.py +++ b/test/test_factory.py @@ -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() diff --git a/test/test_neighborhood.py b/test/test_neighborhood.py index 6a95dfc..0e31226 100644 --- a/test/test_neighborhood.py +++ b/test/test_neighborhood.py @@ -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