neuropercolation/cellular_automaton/display.py
2021-01-09 12:51:20 +00:00

168 lines
7.3 KiB
Python

"""
Copyright 2019 Richard Feistenauer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# pylint: disable=all
import time
import operator
import collections
import contextlib
from typing import Sequence
from . import CellularAutomaton
_Rect = collections.namedtuple(typename="Rect",
field_names=["left", "top", "width", "height"])
class PygameEngine:
""" This is an wrapper for the pygame engine.
By initializing pygame lazy the dependency can be dropped.
"""
def __init__(self, window_size, *args, **kwargs):
super().__init__(*args, **kwargs)
import pygame
self._pygame = pygame
self._pygame.init()
pygame.display.set_caption("Cellular Automaton")
self.__screen = pygame.display.set_mode(window_size)
self.__font = pygame.font.SysFont("monospace", 15)
self._width = window_size[0]
self._height = window_size[1]
def write_text(self, pos, text, color=(0, 255, 0)):
label = self.__font.render(text, True, color)
update_rect = self.__screen.blit(label, pos)
self.update_rectangles(update_rect)
def fill_surface_with_color(self, rect, color=(0, 0, 0)):
return self.__screen.fill(color, rect)
def update_rectangles(self, rectangles):
self._pygame.display.update(rectangles)
def is_active(self): # pragma: no cover
for event in self._pygame.event.get():
if event.type == self._pygame.QUIT:
return False
return True
class CAWindow:
def __init__(self,
cellular_automaton: CellularAutomaton,
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] - 30)
self.__calculate_cell_display_size(stretch_cells)
self.__draw_engine = PygameEngine(window_size) 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)
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) \
and self._cellular_automaton.active
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],
self.__rect.height / grid_dimension[1]]
else:
self.__cell_size = [int(self.__rect.width / grid_dimension[0]),
int(self.__rect.height / grid_dimension[1])]
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)
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, coordinate):
return list(map(operator.mul, self.__cell_size, coordinate))
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()