diff --git a/neuropercolation/__init__.py b/neuropercolation/__init__.py index 0d08251..e983c43 100644 --- a/neuropercolation/__init__.py +++ b/neuropercolation/__init__.py @@ -18,4 +18,4 @@ limitations under the License. from .neighborhood import Neighborhood, MooreNeighborhood, RadialNeighborhood, VonNeumannNeighborhood, \ HexagonalNeighborhood, EdgeRule from .automaton import Neuropercolation, NeuropercolationCoupled -from .display import Simulate2Layers +from .display import Simulate2Layers, Simulate4Layers diff --git a/neuropercolation/automaton.py b/neuropercolation/automaton.py index b04f316..67a3653 100644 --- a/neuropercolation/automaton.py +++ b/neuropercolation/automaton.py @@ -123,7 +123,7 @@ class Neuropercolation(CellularAutomatonCreator, abc.ABC): for coord, old, new in zip(this_state.keys(), this_state.values(), next_state.values()): coord_c = tuple([*coord[:2],int(1-coord[2])]) old_c = this_state[coord_c] - new_state = evolution_rule(old.state.copy(), old_c.state.copy(), [n.state for n in old.neighbors], coord[2], coord_c[2]) #inverse the inhibitory layer's action + new_state = evolution_rule(old.state.copy(), old_c.state.copy(), [n.state[0] for n in old.neighbors], coord[2], coord_c[2]) #inverse the inhibitory layer's action evolve_cell(old, new, new_state) @@ -136,9 +136,9 @@ class Neuropercolation(CellularAutomatonCreator, abc.ABC): def evolve_rule(self, last_cell_state, link_last_state, neighbors_last_states, cell_lay, link_cell_lay): new_cell_state = last_cell_state if link_cell_lay==0: - alive_neighbours = self.__count_alive_neighbours(neighbors_last_states)+link_last_state[0] # adjust for excitatory link cells + alive_neighbours = sum(neighbors_last_states)+link_last_state[0] # adjust for excitatory link cells else: - alive_neighbours = self.__count_alive_neighbours(neighbors_last_states)+(1-link_last_state[0]) # adjust for inhibitory link cells + alive_neighbours = sum(neighbors_last_states)+(1-link_last_state[0]) # adjust for inhibitory link cells CASE = (random.random()>=self.epsilon) if alive_neighbours > 2: @@ -153,14 +153,6 @@ class Neuropercolation(CellularAutomatonCreator, abc.ABC): new_cell_state = ALIVE return new_cell_state - @staticmethod - def __count_alive_neighbours(neighbours): - alive_neighbors = [] - for n in neighbours: - if n == ALIVE: - alive_neighbors.append(1) - return len(alive_neighbors) - class NeuropercolationCoupled(CellularAutomatonCreator, abc.ABC): def __init__(self, dim, eps, coupling=[], *args, **kwargs): @@ -206,11 +198,11 @@ class NeuropercolationCoupled(CellularAutomatonCreator, abc.ABC): if coord[:2] in self.coupling: coord_c = tuple([*coord[:2],coord[2],int(1-coord[3])]) old_c = this_state[coord_c] - new_state = evolution_rule(old.state.copy(), old_c.state.copy(), [n.state for n in old.neighbors], 0) + new_state = evolution_rule(old.state.copy(), old_c.state.copy(), [n.state[0] for n in old.neighbors], 0) else: coord_c = tuple([*coord[:2],int(1-coord[2]),coord[3]]) old_c = this_state[coord_c] - new_state = evolution_rule(old.state.copy(), old_c.state.copy(), [n.state for n in old.neighbors], coord_c[2]) #inverse the inhibitory layer's action + new_state = evolution_rule(old.state.copy(), old_c.state.copy(), [n.state[0] for n in old.neighbors], coord_c[2]) #inverse the inhibitory layer's action evolve_cell(old, new, new_state) @@ -223,9 +215,9 @@ class NeuropercolationCoupled(CellularAutomatonCreator, abc.ABC): def evolve_rule(self, last_cell_state, link_last_state, neighbors_last_states, other_layer): new_cell_state = last_cell_state if other_layer==0: - alive_neighbours = self.__count_alive_neighbours(neighbors_last_states)+link_last_state[0] # adjust for excitatory link cells + alive_neighbours = sum(neighbors_last_states)+link_last_state[0] # adjust for excitatory link cells else: - alive_neighbours = self.__count_alive_neighbours(neighbors_last_states)+(1-link_last_state[0]) # adjust for inhibitory link cells + alive_neighbours = sum(neighbors_last_states)+(1-link_last_state[0]) # adjust for inhibitory link cells CASE = (random.random()>=self.epsilon) if alive_neighbours > 2: @@ -238,4 +230,4 @@ class NeuropercolationCoupled(CellularAutomatonCreator, abc.ABC): new_cell_state = DEAD else: new_cell_state = ALIVE - return new_cell_state \ No newline at end of file + return new_cell_state diff --git a/neuropercolation/display.py b/neuropercolation/display.py index 9e88d55..6619826 100644 --- a/neuropercolation/display.py +++ b/neuropercolation/display.py @@ -113,6 +113,7 @@ class Simulate2Layers: self.__draw_engine._pygame.quit() except: print('Failed to quit pygame') + def _sleep_to_keep_rate(self, time_taken, evolutions_per_second): # pragma: no cover if evolutions_per_second > 0: rest_time = 1.0 / evolutions_per_second - time_taken @@ -173,3 +174,116 @@ class Simulate2Layers: def _is_not_user_terminated(self): return self.__draw_engine.is_active() + +class Simulate4Layers: + def __init__(self, + cellular_automaton: NeuropercolationCoupled, + window_size=(1000, 800), + stretch_cells=False, + draw_engine=None, + state_to_color_cb=None, + *args, **kwargs): + """ + Creates a window to render a 2D CellularAutomaton. + :param cellular_automaton: The automaton to display and evolve + :param window_size: The Window size (default: 1000 x 800) + :param stretch_cells: Stretches cells to fit into window size. (default: false) + Activating it can result in black lines throughout the automaton. + :param draw_engine: The draw_engine (default: pygame) + :param state_to_color_cb: A callback to define the draw color of CA states (default: red for states != 0) + """ + super().__init__(*args, **kwargs) + self._cellular_automaton = cellular_automaton + self.__rect = _Rect(left=0, top=30, width=window_size[0], height=window_size[1]) + self.__calculate_cell_display_size(stretch_cells) + self.__draw_engine = PygameEngine(window_size, self.__rect.top) if draw_engine is None else draw_engine + self.__state_to_color = self._get_cell_color if state_to_color_cb is None else state_to_color_cb + + def run(self, + evolutions_per_second=0, + evolutions_per_draw=1, + last_evolution_step=0,): + """ + Evolves and draws the CellularAutomaton + :param evolutions_per_second: 0 = as fast as possible | > 0 to slow down the CellularAutomaton + :param evolutions_per_draw: Amount of evolutions done before screen gets redrawn. + :param last_evolution_step: 0 = infinite | > 0 evolution step at which this method will stop + Warning: is blocking until finished + """ + with contextlib.suppress(KeyboardInterrupt): + while self._is_not_user_terminated() and self._not_at_the_end(last_evolution_step): + time_ca_start = time.time() + self._cellular_automaton.evolve(evolutions_per_draw) + time_ca_end = time.time() + self._redraw_dirty_cells() + time_ds_end = time.time() + self.print_process_info(evolve_duration=(time_ca_end - time_ca_start), + draw_duration=(time_ds_end - time_ca_end), + evolution_step=self._cellular_automaton.evolution_step) + self._sleep_to_keep_rate(time.time() - time_ca_start, evolutions_per_second) + try: + self.__draw_engine._pygame.quit() + except: + print('Failed to quit pygame') + + def _sleep_to_keep_rate(self, time_taken, evolutions_per_second): # pragma: no cover + if evolutions_per_second > 0: + rest_time = 1.0 / evolutions_per_second - time_taken + if rest_time > 0: + time.sleep(rest_time) + + def _not_at_the_end(self, last_evolution_step): + return (self._cellular_automaton.evolution_step < last_evolution_step or last_evolution_step <= 0) + + def __calculate_cell_display_size(self, stretch_cells): # pragma: no cover + grid_dimension = self._cellular_automaton.dimension + if stretch_cells: + self.__cell_size = [(self.__rect.width) / (grid_dimension[0]*2), + (self.__rect.height) / (grid_dimension[1]*2)] + else: + self.__cell_size = [int((self.__rect.width) / (grid_dimension[0]*2)), + int((self.__rect.height) / (grid_dimension[1]*2))] + + def _redraw_dirty_cells(self): + self.__draw_engine.update_rectangles(list(self.__redraw_dirty_cells())) + + def __redraw_dirty_cells(self): + for coordinate, cell in self._cellular_automaton.cells.items(): + if cell.is_dirty: + yield self.__redraw_cell(cell, coordinate) + + def __redraw_cell(self, cell, coordinate): + cell_color = self.__state_to_color(cell.state) + if coordinate[2]==1: + cell_color = cell_color[::-1] + cell_pos = self.__calculate_cell_position_in_the_grid(coordinate) + surface_pos = self.__calculate_cell_position_on_screen(cell_pos) + cell.is_dirty = False + return self.__draw_cell_surface(surface_pos, cell_color) + + def _get_cell_color(self, current_state: Sequence) -> Sequence: + """ Returns the color of the cell depending on its current state """ + return 255 if current_state[0] else 0, 0, 0 + + def __calculate_cell_position_in_the_grid(self, coord): + return list(map(operator.add, + map(operator.mul, + self.__cell_size, + coord[:2]), + [(self.__rect.width/2)*(coord[3]), + (self.__rect.height/2)*(coord[2])])) + + def __calculate_cell_position_on_screen(self, cell_pos): + return [self.__rect.left + cell_pos[0], self.__rect.top + cell_pos[1]] + + def __draw_cell_surface(self, surface_pos, cell_color): + return self.__draw_engine.fill_surface_with_color((surface_pos, self.__cell_size), cell_color) + + def print_process_info(self, evolve_duration, draw_duration, evolution_step): + self.__draw_engine.fill_surface_with_color(((0, 0), (self.__rect.width, 30))) + self.__draw_engine.write_text((10, 5), "CA: " + "{0:.4f}".format(evolve_duration) + "s") + self.__draw_engine.write_text((310, 5), "Display: " + "{0:.4f}".format(draw_duration) + "s") + self.__draw_engine.write_text((660, 5), "Step: " + str(evolution_step)) + + def _is_not_user_terminated(self): + return self.__draw_engine.is_active()