diff --git a/scripts/main_ui.py b/scripts/main_ui.py index 50e1db2..3024df3 100644 --- a/scripts/main_ui.py +++ b/scripts/main_ui.py @@ -31,8 +31,8 @@ class WorldGeneratorWindow: 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.is_dirty(): + for cell in self._cellular_automaton.grid.get_cells().values(): + if not cell.is_set_for_redrawing(): continue cell_coordinate = cell.coordinate status = cell.get_status_for_iteration(self._cellular_automaton.get_iteration_index()) @@ -45,7 +45,7 @@ class WorldGeneratorWindow: 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))) - cell.release_dirty() + cell.release_from_redraw() pygame.display.update(surfaces_to_update) def main_loop(self): @@ -53,7 +53,7 @@ class WorldGeneratorWindow: while running: time_ca_start = time.time() - self._cellular_automaton.evolve() + self._cellular_automaton.evolve_x_times(10) time_ca_end = time.time() self._display_cellular_automaton() time_ds_end = time.time() @@ -86,17 +86,17 @@ class TestRule(Rule): cell.set_status_for_iteration([rand], iteration_index + 1) if rand != 0: active = True - cell.set_dirty() + cell.set_for_redraw() elif len(neighbors) == 8: left_neighbour_status = neighbors[3].get_status_for_iteration(last_iteration) active = cell.set_status_for_iteration(left_neighbour_status, iteration_index) if active: - cell.set_dirty() + cell.set_for_redraw() return active if __name__ == "__main__": rule = TestRule() - ca = CellularAutomaton([10, 10], MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS), rule, thread_count=2) + ca = CellularAutomaton([400, 400], MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS), rule) ca_window = WorldGeneratorWindow([1000, 730], ca) ca_window.main_loop() diff --git a/src/cellular_automaton/ca_cell.py b/src/cellular_automaton/ca_cell.py index c725c9d..81a87a7 100644 --- a/src/cellular_automaton/ca_cell.py +++ b/src/cellular_automaton/ca_cell.py @@ -7,19 +7,22 @@ class Cell: self._dirty = False def set_neighbours(self, neighbours: list): + """ Set new cells as neighbour of this cell. + :param neighbours: A List of Cell names. + """ self.neighbours = neighbours - def is_dirty(self): + def is_set_for_redrawing(self): return self._dirty - def set_dirty(self): + def set_for_redraw(self): self._dirty = True - def release_dirty(self): + def release_from_redraw(self): self._dirty = False - def set_status_for_iteration(self, new_status, iteration): - """ Will set the new status for Iteration. + def set_status_of_iteration(self, new_status, iteration): + """ Will set the new status for the iteration modulo two. :param new_status: The new status to set. :param iteration: Uses the iteration index, to differ between current and next state. :return True if status has changed. @@ -28,8 +31,8 @@ class Cell: return self._status[0] != self._status[1] - def get_status_for_iteration(self, iteration): - """ Will return the status for the iteration. + def get_status_of_iteration(self, iteration): + """ Will return the status for the iteration modulo two. :param iteration: Uses the iteration index, to differ between current and next state. :return The status for this iteration. """ diff --git a/src/cellular_automaton/ca_grid.py b/src/cellular_automaton/ca_grid.py index 33f54d4..e2b6b6b 100644 --- a/src/cellular_automaton/ca_grid.py +++ b/src/cellular_automaton/ca_grid.py @@ -1,5 +1,3 @@ -import sys - from cellular_automaton.ca_cell import Cell from cellular_automaton.ca_neighborhood import Neighborhood @@ -16,19 +14,24 @@ class Grid: self._active_cells = self._cells.copy() self._set_all_cells_active() - def get_names_of_active_cells(self): + def get_active_cell_names(self): return list(self._active_cells.keys()) def get_active_cells(self): return self._active_cells - def clear_active_cells(self): - self._active_cells.clear() + def get_cells(self): + return self._cells - 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 clear_active_cells(self): + self._active_cells = {} + + def set_cells_active(self, cells: list): + """ Consider the cells in the next evolution cycle. + :param cells: A list of Cell objects, that shall be considered in the next evolution cycle. + """ + for cell in cells: + self._active_cells[cell.name] = cells def get_cell_and_neighbors(self, cell_name): cell = self._cells[cell_name] @@ -84,18 +87,10 @@ class Grid: try: self._recursive_step_down_dimensions(coordinate.copy(), dimension_index, self._set_cell_neighbours) except IndexError: - neighbours_coordinates = self._neighborhood.get_neighbor_coordinates(coordinate, self._dimension) + neighbours_coordinates = self._neighborhood.calculate_cell_neighbor_coordinates(coordinate, self._dimension) neighbour_names = [self._cells[_join_coordinate(nc)].name for nc in neighbours_coordinates] 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: diff --git a/src/cellular_automaton/ca_neighborhood.py b/src/cellular_automaton/ca_neighborhood.py index 809c393..63f6fc6 100644 --- a/src/cellular_automaton/ca_neighborhood.py +++ b/src/cellular_automaton/ca_neighborhood.py @@ -9,19 +9,28 @@ class EdgeRule(Enum): class Neighborhood: def __init__(self, neighbors: 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. + """ self._neighbors = neighbors self.edge_rule = edge_rule - self.dimensions = [] + self.grid_dimensions = [] def get_relative_neighbor_coordinates(self): return self._neighbors - def get_neighbor_coordinates(self, cell_coordinate, dimensions): - self.dimensions = dimensions + 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 + :param grid_dimensions: The dimensions of the grid, to apply edge the rule. + :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_of(cell_coordinate) + return self._apply_edge_rule_to_neighbours(cell_coordinate) 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): @@ -29,37 +38,42 @@ class Neighborhood: return False def _is_coordinate_on_an_edge(self, coordinate): - for nd, d in zip(coordinate, self.dimensions): - if nd == 0 or nd == d - 1: + for neighbor_dimension, dimension in zip(coordinate, self.grid_dimensions): + if neighbor_dimension == 0 or neighbor_dimension == dimension - 1: return True return False - def _apply_edge_rule_to_neighbours_of(self, cell_coordinate): + 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, cell_coordinate): - remaining_neighbours.append(self._calculate_neighbour_coordinate(neighbour, cell_coordinate)) + 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, cell_coordinate): + def _does_ignore_edge_cell_neighbours_rule_apply(self, neighbour, coordinate): if self.edge_rule == EdgeRule.IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS: - for rel_nd, cd, d in zip(neighbour, cell_coordinate, self.dimensions): - nd = cd + rel_nd - if nd < 0 or nd >= d: + 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_nd, cd, d in zip(neighbour, cell_coordinate, self.dimensions): - nd = cd + rel_nd - if nd < 0: - nd = d - 1 - elif nd >= d: - nd = 0 - new_coordinate.append(nd) + 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 + class MooreNeighborhood(Neighborhood): def __init__(self, edge_rule: EdgeRule): diff --git a/src/cellular_automaton/cellular_automaton.py b/src/cellular_automaton/cellular_automaton.py index b8fafe8..3ce68c2 100644 --- a/src/cellular_automaton/cellular_automaton.py +++ b/src/cellular_automaton/cellular_automaton.py @@ -1,81 +1,62 @@ -import multiprocessing -import time - from cellular_automaton.ca_grid import Grid from cellular_automaton.ca_rule import Rule from cellular_automaton.ca_neighborhood import Neighborhood class CellularAutomaton: - def __init__(self, dimension: list, neighborhood: Neighborhood, rule_: Rule=None, thread_count: int=4): + def __init__(self, dimension: list, neighborhood: Neighborhood, evolution_rule: Rule=None): self.grid = Grid(dimension, neighborhood) - self.rule = rule_ - self._thread_count = thread_count + self.evolution_rule = evolution_rule self.iteration = 0 self.test_number = 0 def set_rule(self, rule: Rule): - self.rule = rule - - def set_thread_count(self, thread_count: int): - self._thread_count = thread_count + """ Set new evolution rule. + :param rule: + :return: + """ + self.evolution_rule = rule def get_iteration_index(self): + """ Get the count of evolution cycles done. + :return: Evolution steps done. + """ return self.iteration + def evolve_x_times(self, evolutions: int): + """ Evolve all cells for x time steps. + :param evolutions: the count of evolutions done. + :return: True if all cells are inactive + """ + for evo in range(evolutions): + finished = self.evolve() + if finished: + return True + return False + def evolve(self): """ Evolves all active cells for one time step. :return: True if all cells are inactive. """ - if self._all_cells_are_inactive(): + if self._is_evolution_finished(): return True else: self.iteration += 1 - self._start_multi_process_cell_evolution() + self._evolve_all_active_cells() return False - def _start_multi_process_cell_evolution(self): - """ Evolves the cells in separate threads """ - lists_of_cell_names = self._create_list_of_cell_names_lists_for_all_threads() + def _evolve_all_active_cells(self): + active_cells = self.grid.get_active_cell_names() self.grid.clear_active_cells() - jobs = self._start_treads_to_evolve_grid(lists_of_cell_names) - self._wait_for_all_threads_to_finish(jobs) - print(self.test_number) + self._evolve_cells(active_cells) - def _all_cells_are_inactive(self): - return len(self.grid.get_names_of_active_cells()) == 0 + def _is_evolution_finished(self): + return len(self.grid.get_active_cell_names()) == 0 - def _create_list_of_cell_names_lists_for_all_threads(self): - active_cells = self.grid.get_names_of_active_cells() - cell_count_per_thread = int(len(active_cells) / self._thread_count) - return self._divide_list_in_parts_of_size(active_cells, cell_count_per_thread) + def _evolve_cells(self, cell_names: list): + for cell_name in cell_names: + cell_info = self.grid.get_cell_and_neighbors(cell_name) + active = self.evolution_rule.evolve_cell(cell_info[0], cell_info[1], self.iteration) - @staticmethod - def _divide_list_in_parts_of_size(active_cells: list, cell_count_per_thread: int): - return [active_cells[i:i + cell_count_per_thread] - for i in range(0, len(active_cells), cell_count_per_thread)] - - def _start_treads_to_evolve_grid(self, lists_of_cell_names): - jobs = [] - for t in range(self._thread_count): - process = multiprocessing.Process(target=_evolve_cells, args=(self, lists_of_cell_names[t])) - jobs.append(process) - process.start() - return jobs - - @staticmethod - def _wait_for_all_threads_to_finish(jobs): - for process in jobs: - process.join() - - -def _evolve_cells(cellular_automaton, cell_names: list): - time.sleep(2) - cellular_automaton.test_number += 1 - print(cellular_automaton.grid) - for cell_name in cell_names: - cell_info = cellular_automaton.grid.get_cell_and_neighbors(cell_name) - active = cellular_automaton.rule.evolve_cell(cell_info[0], cell_info[1], cellular_automaton.iteration) - - if active: - cellular_automaton.grid.set_cell_and_neighbours_active(cell_info) + if active: + self.grid.set_cells_active(cell_info[0] + cell_info[1])