diff --git a/performance.txt b/performance.txt new file mode 100644 index 0000000..3bf28aa --- /dev/null +++ b/performance.txt @@ -0,0 +1,11 @@ +# With 100x100 10 times + +Try One +TOTAL TIME: 0.1217s +SIZE: 21.7525MB + +TOTAL TIME: 0.1171s # Only set on change +SIZE: 21.7525MB # process size 51,4 / main(75,9) + +TOTAL TIME: 0.1161s +SIZE: 20.3338MB # removed grid \ No newline at end of file diff --git a/scripts/main_ui.py b/scripts/main_ui.py index 6bc58e1..46505d8 100644 --- a/scripts/main_ui.py +++ b/scripts/main_ui.py @@ -39,14 +39,16 @@ if __name__ == "__main__": from cellular_automaton.ca_neighborhood import MooreNeighborhood, EdgeRule from cellular_automaton.ca_display import PyGameFor2D from cellular_automaton.ca_grid import Grid + freeze_support() random.seed(1000) + dimension = [100, 100] rule = TestRule() - grid = Grid(dimension=[200, 200], # best is 400/400 with 0,2 ca speed and 0,09 redraw + grid = Grid(dimension=dimension, # best is 400/400 with 0,2 ca speed and 0,09 redraw neighborhood=MooreNeighborhood(EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS), state_class=MyState) - ca = CellularAutomaton(grid, rule) + ca = CellularAutomaton(tuple(grid.get_cells().values()), dimension, rule) ca_window = PyGameFor2D(window_size=[1000, 800], cellular_automaton=ca) - ca_processor = CellularAutomatonProcessor(process_count=2, cellular_automaton=ca) + ca_processor = CellularAutomatonProcessor(process_count=4, cellular_automaton=ca) ca_window.main_loop(cellular_automaton_processor=ca_processor, ca_iterations_per_draw=5) diff --git a/src/cellular_automaton/ca_cell.py b/src/cellular_automaton/ca_cell.py index 67a8192..53f1e82 100644 --- a/src/cellular_automaton/ca_cell.py +++ b/src/cellular_automaton/ca_cell.py @@ -1,8 +1,9 @@ from cellular_automaton.ca_cell_state import CellState +from typing import Type class Cell: - def __init__(self, state_class: CellState.__class__, coordinate: list): + def __init__(self, state_class: Type[CellState], coordinate: list): self._coordinate = coordinate self._state = state_class() self._neighbours = [] @@ -16,29 +17,20 @@ class Cell: def get_coordinate(self): return self._coordinate - def evolve_if_ready(self, rule): - if self._neighbours_are_younger(): - if self._state.is_active(): - new_state = rule(self._state.get_current_state(), self.get_neighbour_states()) - self.set_new_state_and_activate(new_state) + def evolve_if_ready(self, rule, iteration): + if self._state.is_active(iteration): + new_state = rule(self._state.get_state_of_last_iteration(iteration), self.get_neighbour_states(iteration)) + self.set_new_state_and_activate(new_state, iteration) - self._state.increase_age() + def get_neighbour_states(self, index): + return [n.get_state_of_last_iteration(index) for n in self._neighbours] - def _neighbours_are_younger(self): - for n in self._neighbours: - if n.get_age() < self._state.get_age(): - return False - return True - - def get_neighbour_states(self): - return [n.get_state_of_iteration(self._state.get_age()) for n in self._neighbours] - - def set_new_state_and_activate(self, new_state: CellState): - changed = self._state.set_current_state(new_state) + def set_new_state_and_activate(self, new_state: CellState, iteration): + changed = self._state.set_state_of_iteration(new_state, iteration) if changed: - self._set_active() + self._set_active_for_next_iteration(iteration) - def _set_active(self): - self._state.set_active_for_next_iteration(self._state.get_age() + 1) + def _set_active_for_next_iteration(self, iteration): + self._state.set_active_for_next_iteration(iteration) for n in self._neighbours: - n.set_active_for_next_iteration(self._state.get_age() + 1) + n.set_active_for_next_iteration(iteration) diff --git a/src/cellular_automaton/ca_cell_state.py b/src/cellular_automaton/ca_cell_state.py index d8c0ca3..706a928 100644 --- a/src/cellular_automaton/ca_cell_state.py +++ b/src/cellular_automaton/ca_cell_state.py @@ -1,4 +1,5 @@ -from multiprocessing import Array, Value +from multiprocessing import RawArray, RawValue +from ctypes import c_float, c_bool class CellState: @@ -8,45 +9,35 @@ class CellState: When using the cellular automaton display, inherit this class and implement get_state_draw_color. """ def __init__(self, initial_state=(0., ), draw_first_state=True): - self._state_slots = [Array('d', initial_state) for i in range(self.__class__._state_save_slot_count)] - self._active = Value('i', 1) - self._age = Value('i', 0) + self._state_slots = [RawArray(c_float, initial_state) for i in range(self.__class__._state_save_slot_count)] + self._active = [RawValue(c_bool, False) for i in range(self.__class__._state_save_slot_count)] + self._active[0].value = True if draw_first_state: - self._dirty = Value('i', 1) + self._dirty = RawValue(c_bool, True) else: - self._dirty = Value('i', 0) - - def get_age(self): - return self._age.value - - def get_current_state(self): - return self.get_state_of_iteration(self.get_age()) + self._dirty = RawValue(c_bool, False) def get_state_of_iteration(self, iteration): """ Will return the state for the iteration modulo number of saved states. :param iteration: Uses the iteration index, to differ between concurrent states. :return The state for this iteration. """ - return self._state_slots[iteration % self.__class__._state_save_slot_count] + return self._state_slots[self.__calculate_slot(iteration)] - def is_active(self): - return self._active.value > self._age.value + def __calculate_slot(self, iteration): + return iteration % self.__class__._state_save_slot_count + + def is_active(self, iteration): + return self._active[self.__calculate_slot(iteration)] def set_active_for_next_iteration(self, iteration): - self._active.value = max(self._active.value, iteration + 1) - - def increase_age(self): - with self._age.get_lock(): - self._age.value += 1 + self._active[self.__calculate_slot(iteration + 1)].value = True def is_set_for_redraw(self): - return self._dirty != 1 + return self._dirty.value def was_redrawn(self): - self._dirty = 0 - - def set_current_state(self, new_state): - return self.set_state_of_iteration(new_state, self.get_age() + 1) + self._dirty.value = False def get_state_of_last_iteration(self, current_iteration_index): return self.get_state_of_iteration(current_iteration_index - 1) @@ -65,16 +56,13 @@ class CellState: try: if current_state[i] != new_state[i]: changed = True - - current_state[i] = new_state[i] + current_state[i] = new_state[i] except IndexError: raise IndexError("New State length or type is invalid!") self._dirty.value |= changed + self._active[self.__calculate_slot(iteration)].value = False return changed def get_state_draw_color(self, iteration): raise NotImplementedError - - def __str__(self): - return str(self._state_slots) diff --git a/src/cellular_automaton/ca_display.py b/src/cellular_automaton/ca_display.py index a2c7d0e..d1b5df5 100644 --- a/src/cellular_automaton/ca_display.py +++ b/src/cellular_automaton/ca_display.py @@ -2,6 +2,11 @@ import pygame import time import operator +import cProfile +import pstats +from pympler import asizeof + + from cellular_automaton.cellular_automaton import CellularAutomaton, CellularAutomatonProcessor @@ -19,16 +24,13 @@ class DisplayFor2D: cell_size = self._calculate_cell_display_size(grid_rect[-2:]) self._display_info = _DisplayInfo(grid_rect[-2:], grid_rect[:2], cell_size, screen) - def set_cellular_automaton(self, cellular_automaton): - self._cellular_automaton = cellular_automaton - - def _redraw_cellular_automaton(self): - pygame.display.update(list(_cell_redraw_rectangles(self._cellular_automaton.grid.get_cells().values(), - self._cellular_automaton.evolution_iteration_index, + def redraw_cellular_automaton(self): + pygame.display.update(list(_cell_redraw_rectangles(self._cellular_automaton.cells, + 0, self._display_info))) def _calculate_cell_display_size(self, grid_size): - grid_dimension = self._cellular_automaton.grid.get_dimension() + grid_dimension = self._cellular_automaton.dimension return list(map(operator.truediv, grid_size, grid_dimension)) @@ -36,7 +38,7 @@ class PyGameFor2D: def __init__(self, window_size: list, cellular_automaton: CellularAutomaton): self._window_size = window_size self._cellular_automaton = cellular_automaton - + self._cellular_automaton_proocessor = None pygame.init() pygame.display.set_caption("Cellular Automaton") self._screen = pygame.display.set_mode(self._window_size) @@ -56,20 +58,34 @@ class PyGameFor2D: def main_loop(self, cellular_automaton_processor: CellularAutomatonProcessor, ca_iterations_per_draw): running = True - + cellular_automaton_processor.evolve() + first = True while running: + pygame.event.get() time_ca_start = time.time() + if first: + self._evolve_with_performance(cellular_automaton_processor, time_ca_start) + first = False + else: + cellular_automaton_processor.evolve() time_ca_end = time.time() - self.ca_display._redraw_cellular_automaton() + self.ca_display.redraw_cellular_automaton() time_ds_end = time.time() self._print_process_duration(time_ca_end, time_ca_start, time_ds_end) - time.sleep(0.5) - - for event in pygame.event.get(): - if event.type == pygame.QUIT: - cellular_automaton_processor.stop() - running = False + def _evolve_with_performance(self, cap, time_ca_start): + size = asizeof.asizeof(self._cellular_automaton) + cProfile.runctx("cap.evolve_x_times(10)", None, locals(), "performance_test") + print("PERFORMANCE") + p = pstats.Stats('performance_test2') + p.strip_dirs() + # sort by cumulative time in a function + p.sort_stats('cumulative').print_stats(10) + # sort by time spent in a function + p.sort_stats('time').print_stats(10) + time_ca_end = time.time() + print("TOTAL TIME: " + "{0:.4f}".format(time_ca_end - time_ca_start) + "s") + print("SIZE: " + "{0:.4f}".format(size / (1024 * 1024)) + "MB") def _cell_redraw_rectangles(cells, evolution_index, display_info): diff --git a/src/cellular_automaton/ca_neighborhood.py b/src/cellular_automaton/ca_neighborhood.py index 63f6fc6..967b17c 100644 --- a/src/cellular_automaton/ca_neighborhood.py +++ b/src/cellular_automaton/ca_neighborhood.py @@ -17,9 +17,6 @@ class Neighborhood: self.edge_rule = edge_rule self.grid_dimensions = [] - def get_relative_neighbor_coordinates(self): - return self._neighbors - def calculate_cell_neighbor_coordinates(self, cell_coordinate, grid_dimensions): """ Get a list of coordinates for the cell neighbors. The EdgeRule can reduce the returned neighbor count. :param cell_coordinate: The coordinate of the cell to get the neighbors diff --git a/src/cellular_automaton/cellular_automaton.py b/src/cellular_automaton/cellular_automaton.py index 7b9f3c7..11eca46 100644 --- a/src/cellular_automaton/cellular_automaton.py +++ b/src/cellular_automaton/cellular_automaton.py @@ -1,39 +1,49 @@ import multiprocessing -from cellular_automaton.ca_grid import Grid from cellular_automaton.ca_rule import Rule class CellularAutomaton: - def __init__(self, grid: Grid, evolution_rule: Rule): - self.grid = grid + def __init__(self, cells, dimension, evolution_rule: Rule): + self.cells = cells + self.dimension = dimension self.evolution_rule = evolution_rule - self.evolution_iteration_index = 0 + self.evolution_iteration_index = multiprocessing.RawValue('i', -1) class CellularAutomatonProcessor: def __init__(self, cellular_automaton, process_count: int = 1): - self.active = multiprocessing.Value('i', 1) - cells = list(cellular_automaton.grid.get_cells().values()) - chunk_size = int(len(cells) / process_count) - self._processes = [multiprocessing.Process(target=_process_routine, - name=str(i), - args=(cells[i*chunk_size:i*chunk_size + chunk_size], - cellular_automaton.evolution_rule, - self.active)) - for i in range(process_count)] - for p in self._processes: - p.start() - self.__cellular_automaton = None + self.ca = cellular_automaton + self.evolve_range = range(len(self.ca.cells)) + self.pool = multiprocessing.Pool(processes=process_count, + initializer=_init_process, + initargs=(self.ca.cells, + self.ca.evolution_rule, + self.ca.evolution_iteration_index)) + for cell in self.ca.cells: + cell.set_neighbours(None) - def stop(self): - self.active.value = 0 - for p in self._processes: - p.join() + def evolve_x_times(self, x): + for x in range(x): + self.evolve() + + def evolve(self): + self.ca.evolution_iteration_index.value += 1 + self.pool.map(_process_routine, self.evolve_range) -def _process_routine(cells, rule, active): - while active.value == 1: - for cell in cells: - cell.evolve_if_ready(rule.evolve_cell) +global_cells = None +global_rule = None +global_iteration = None + + +def _init_process(cells, rule, index): + global global_rule, global_cells, global_iteration + global_cells = cells + global_rule = rule + global_iteration = index + + +def _process_routine(i): + global_cells[i].evolve_if_ready(global_rule.evolve_cell, global_iteration.value) diff --git a/test/test_cell.py b/test/test_cell.py index 30e2f67..17ca8ac 100644 --- a/test/test_cell.py +++ b/test/test_cell.py @@ -1,12 +1,41 @@ import sys sys.path.append('../src') +import cellular_automaton.ca_cell_state as cas import cellular_automaton.ca_cell as cac import unittest +class TestState(cas.CellState): + def __init__(self): + super().__init__() + + class TestCellState(unittest.TestCase): - pass + def setUp(self): + self.cell = cac.Cell(TestState, []) + self.neighbours = [TestState() for x in range(5)] + for neighbour in self.neighbours: + neighbour.set_state_of_iteration((0, ), 0) + self.cell.set_neighbours(self.neighbours) + + def cell_and_neighbours_active(self, iteration): + self.neighbours.append(self.cell.get_state()) + all_active = True + for state in self.neighbours: + if not state.is_active(iteration): + all_active = False + return all_active + + def test_evolve_activation(self): + self.cell.evolve_if_ready((lambda a, b: (1,)), 0) + all_active = self.cell_and_neighbours_active(1) + self.assertTrue(all_active) + + def test_evolve_activation_on_no_change(self): + self.cell.evolve_if_ready((lambda a, b: (0,)), 0) + all_active = self.cell_and_neighbours_active(1) + self.assertFalse(all_active) if __name__ == '__main__': diff --git a/test/test_cell_state.py b/test/test_cell_state.py index 97984ba..05e03de 100644 --- a/test/test_cell_state.py +++ b/test/test_cell_state.py @@ -22,6 +22,21 @@ class TestCellState(unittest.TestCase): cell_state.set_state_of_iteration(new_state=(1,), iteration=0) self.assertEqual(tuple(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()) + + 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()) + + 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)) + if __name__ == '__main__': unittest.main() diff --git a/test/test_neighborhood.py b/test/test_neighborhood.py new file mode 100644 index 0000000..06bcd9e --- /dev/null +++ b/test/test_neighborhood.py @@ -0,0 +1,40 @@ +import sys +sys.path.append('../src') + +import cellular_automaton.ca_neighborhood as csn +import unittest + + +class TestCellState(unittest.TestCase): + def check_neighbors(self, neighborhood, neighborhood_sets): + 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)) + return False + return True + + def test_ignore_missing_neighbors(self): + neighborhood = csn.MooreNeighborhood(csn.EdgeRule.IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS) + n00 = [[0, 0], [[1, 0], [0, 1], [1, 1]]] + n11 = [[1, 1], [[0, 0], [1, 0], [2, 0], [0, 1], [2, 1], [0, 2], [1, 2], [2, 2]]] + n22 = [[2, 2], [[1, 1], [2, 1], [1, 2]]] + self.assertTrue(self.check_neighbors(neighborhood, [n00, n11, n22])) + + def test_ignore_edge_cells(self): + neighborhood = csn.MooreNeighborhood(csn.EdgeRule.IGNORE_EDGE_CELLS) + n00 = [[0, 0], []] + n11 = [[1, 1], [[0, 0], [1, 0], [2, 0], [0, 1], [2, 1], [0, 2], [1, 2], [2, 2]]] + n22 = [[2, 2], []] + self.assertTrue(self.check_neighbors(neighborhood, [n00, n11, n22])) + + def test_cyclic_dimensions(self): + neighborhood = csn.MooreNeighborhood(csn.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS) + n00 = [[0, 0], [[2, 2], [0, 2], [1, 2], [2, 0], [1, 0], [2, 1], [0, 1], [1, 1]]] + n11 = [[1, 1], [[0, 0], [1, 0], [2, 0], [0, 1], [2, 1], [0, 2], [1, 2], [2, 2]]] + n22 = [[2, 2], [[1, 1], [2, 1], [0, 1], [1, 2], [0, 2], [1, 0], [2, 0], [0, 0]]] + self.assertTrue(self.check_neighbors(neighborhood, [n00, n11, n22])) + + +if __name__ == '__main__': + unittest.main()