Commit bbd6ef12 authored by Karol Actun's avatar Karol Actun
Browse files

added stayontop hint to the remaining windows (save as etc.).

refractored error handling in the visualization.
fixed a small bug with reset on the last vis.run() call.
added the "close_at_end" config for closing the simulator after the algorithm has set the end flag in the world - default is true, only when visualization is on, without the simulator closes always after the algorithm.
parent 8292015a
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/*
.idea*
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
......
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (swarm-sim)" project-jdk-type="Python SDK" />
</project>
\ No newline at end of file
......@@ -16,6 +16,9 @@ particle_random_order_always = False
window_size_x = 1920
window_size_y = 1080
## should the simulation close at the end of the algorithm? (only if the visualization on)
close_at_end = True
[Visualization]
# Visualization 1 = On, 0 = Off
visualization = 1
......
......@@ -3,7 +3,7 @@ from PyQt5.QtWidgets import (QVBoxLayout, QPushButton, QColorDialog, QRadioButto
QSlider, QHBoxLayout, QCheckBox, QTabBar, QLineEdit, QGroupBox, QComboBox, QStyle)
from PyQt5.QtCore import Qt
from lib.vis3d import Visualization
from lib.visualization.utils import show_msg
from lib.visualization.utils import show_msg, Level
from lib.world import World
# global variables for all te functions..
......@@ -78,10 +78,12 @@ def create_slider(tick_interval: int, tick_position: int, max_position: int, min
slider.setMaximum(max_position)
slider.setMinimum(min_position)
slider.setSliderPosition(slider_position)
# noinspection PyUnresolvedReferences
slider.valueChanged.connect(callback)
return slider
# noinspection PyUnresolvedReferences
def sim_tab():
tab = QTabBar()
layout = QVBoxLayout()
......@@ -225,6 +227,7 @@ def sim_tab():
return tab
# noinspection PyUnresolvedReferences
def get_new_matter_color_picker():
cp_button = QPushButton("change color of new matter")
......@@ -242,6 +245,7 @@ def get_new_matter_color_picker():
return cp_button
# noinspection PyUnresolvedReferences
def get_matter_radios():
p_radio = QRadioButton("particle")
p_radio.setChecked(False)
......@@ -285,6 +289,7 @@ def get_rps_slider():
return hbox
# noinspection PyUnresolvedReferences
def vis_tab():
tab = QTabBar()
layout = QVBoxLayout()
......@@ -325,6 +330,7 @@ def vis_tab():
return tab
# noinspection PyUnresolvedReferences
def get_animation_checkbox():
chkbx = QCheckBox("enable")
chkbx.setChecked(vis.get_animation())
......@@ -337,6 +343,7 @@ def get_animation_checkbox():
return chkbx
# noinspection PyUnresolvedReferences
def get_auto_animation():
mss_label = QLabel("speed (%d steps per round):" % vis.get_manual_animation_speed())
......@@ -520,6 +527,7 @@ def get_rota_sens_slider():
return hbox
# noinspection PyUnresolvedReferences
def get_projection_switch():
vbox = QVBoxLayout()
desc = QLabel("projection type:")
......@@ -552,6 +560,7 @@ def get_projection_switch():
return vbox
# noinspection PyUnresolvedReferences
def get_color_picker():
bg_button = QPushButton("background")
......@@ -630,6 +639,7 @@ def get_render_distance_slider():
return vbox
# noinspection PyUnresolvedReferences
def get_vis_show_checkboxes():
center_cb = QCheckBox()
center_cb.setText("show center")
......@@ -665,6 +675,7 @@ def get_vis_show_checkboxes():
return vbox
# noinspection PyUnresolvedReferences
def get_show_checkboxes():
lines_cb = QCheckBox()
lines_cb.setText("show lines")
......@@ -729,6 +740,7 @@ def get_grid_coordinates_scale_slider():
return vbox
# noinspection PyUnresolvedReferences
def recalculate_grid():
hbox = QHBoxLayout()
rec_button = QPushButton("update grid with size:")
......@@ -741,7 +753,7 @@ def recalculate_grid():
if size_edit.text().isnumeric():
vis.recalculate_grid(int(size_edit.text()))
else:
show_msg("Grid size has to be a number.", 2)
show_msg("Grid size has to be a number.", Level.WARNING, vis.get_main_window())
rec_button.clicked.connect(on_click)
......@@ -751,6 +763,7 @@ def recalculate_grid():
return hbox
# noinspection PyUnresolvedReferences
def get_antialiasing_combobox():
hbox = QHBoxLayout()
combo = QComboBox()
......
from lib.vis3d import Visualization
from lib.world import World
......
......@@ -16,22 +16,23 @@ class ConfigData:
self.particle_random_order_always = config.getboolean("Simulator", "particle_random_order_always")
self.window_size_x = config.getint("Simulator", "window_size_x")
self.window_size_y = config.getint("Simulator", "window_size_y")
self.close_at_end = config.getboolean("Simulator", "close_at_end")
self.visualization = config.getint("Visualization", "visualization")
try:
self.gui = config.get("Visualization", "gui")
except configparser.NoOptionError as noe:
except configparser.NoOptionError:
print("no gui option given. setting to default \"gui.py\"")
self.gui = "gui.py"
try:
self.grid_class = config.get("Visualization", "grid_class")
except configparser.NoOptionError as noe:
except configparser.NoOptionError:
raise RuntimeError("Fatal Error: no grid class defined in config.ini!")
try:
self.grid_size = config.getint("Visualization", "grid_size")
except configparser.NoOptionError as noe:
except configparser.NoOptionError:
raise RuntimeError("Fatal Error: no grid size defined in config.ini!")
test = getattr(importlib.import_module("grids.%s" % self.grid_class), self.grid_class)
......@@ -90,14 +91,13 @@ class ConfigData:
try:
self.scenario = config.get("File", "scenario")
except configparser.NoOptionError as noe:
except configparser.NoOptionError:
self.scenario = "init_scenario.py"
try:
self.solution = config.get("File", "solution")
except configparser.NoOptionError as noe:
except configparser.NoOptionError:
self.solution = "solution.py"
self.local_time = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')[:-1]
self.multiple_sim = 0
......@@ -7,7 +7,7 @@ class Location(matter.Matter):
"""In the class matter all the methods for the characterstic of a location is included"""
def __init__(self, world, coordinates, color):
"""Initializing the location constructor"""
super().__init__(world, coordinates, color, type="location", mm_size=world.config_data.location_mm_size)
super().__init__(world, coordinates, color, matter_type="location", mm_size=world.config_data.location_mm_size)
def set_color(self, color):
super().set_color(color)
......
import uuid
from datetime import datetime
from lib.visualization.utils import show_msg
from lib.visualization.utils import Level, VisualizationError
class Matter:
def __init__(self, world, coordinates, color, type=None, mm_size=100):
def __init__(self, world, coordinates, color, matter_type=None, mm_size=100):
"""Initializing the matter constructor"""
self.coordinates = coordinates
self.color = color
self.__id = str(uuid.uuid4())
self.world = world
self._memory = {}
self.type = type
self.type = matter_type
self.memory_limitation = world.config_data.memory_limitation
self.mm_size = mm_size
self.created = False
def read_memory_with(self, key):
"""
Read all its own memory based on a give keywoard
Read all its own memory based on a give keyword
:param key: Keywoard
:return: The founded memory; None: When nothing is written based on the keywoard
:param key: Keyword
:return: The founded memory; None: When nothing is written based on the keyword
"""
tmp_memory = None
if key in self._memory:
tmp_memory = self._memory[key]
self.world.csv_round.update_metrics( memory_read=1)
self.world.csv_round.update_metrics(memory_read=1)
if isinstance(tmp_memory, list) and len(str(tmp_memory)) == 0:
return None
if isinstance(tmp_memory, str) and len(str(tmp_memory)) == 0:
......@@ -37,16 +36,10 @@ class Matter:
def read_whole_memory(self):
"""
Reads all matters own memory based on a give keywoard
:param key: Keywoard
:return: The founded memory; None: When nothing is written based on the keywoard
Reads matters whole memory
:return: The found memory or None
"""
if self._memory != None :
self.world.csv_round.update_metrics(memory_read=1)
return self._memory
else:
return None
return self._memory
def write_memory_with(self, key, data):
"""
......@@ -57,36 +50,34 @@ class Matter:
:return: True: Successful written into the memory; False: Unsuccessful
"""
if (self.memory_limitation == True and len( self._memory) < self.mm_size) or not self.memory_limitation:
if (self.memory_limitation and len(self._memory) < self.mm_size) or not self.memory_limitation:
self._memory[key] = data
self.world.csv_round.update_metrics(memory_write=1)
return True
else:
return False
#write csv
# write csv
def write_memory(self, data):
"""
Write on its own memory a data with a keywoard
:param key: A string keyword for orderring the data into the memory
Write on its own memory a data with timestamp as key
:param data: The data that should be stored into the memory
:return: True: Successful written into the memory; False: Unsuccessful
"""
if (self.memory_limitation == True and len( self._memory) < self.mm_size) or not self.memory_limitation:
self._memory[datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-1]] = data
self.world.csv_round.update_metrics(memory_write=1)
return True
if (self.memory_limitation and len(self._memory) < self.mm_size) or not self.memory_limitation:
self._memory[datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-1]] = data
self.world.csv_round.update_metrics(memory_write=1)
return True
else:
return False
#write csv
# write csv
def delete_memeory_with(self, key):
del self._memory[key]
def delete_whole_memory(self):
self._memory.clear()
self._memory.clear()
def get_id(self):
"""
......@@ -103,7 +94,9 @@ class Matter:
:return: None
"""
if len(color) != 4:
show_msg("Invalid color format!\ncolor has to be in rgba format => (float, float, float, float)", 2)
raise VisualizationError(
"Invalid color format!\ncolor has to be in rgba format => (float, float, float, float)", Level.WARNING)
else:
self.color = color
......
......@@ -19,7 +19,7 @@ class Particle(matter.Matter):
def __init__(self, world, coordinates, color, particle_counter=0):
"""Initializing the particle constructor"""
super().__init__(world, coordinates, color,
type="particle", mm_size=world.config_data.particle_mm_size)
matter_type="particle", mm_size=world.config_data.particle_mm_size)
self.number = particle_counter
self.__isCarried = False
self.carried_tile = None
......@@ -121,13 +121,13 @@ class Particle(matter.Matter):
if self.world.config_data.type == 1:
if abs(direction_coord[0]) > self.world.get_x_size():
direction_coord = (
-1 * (self.coordinates[0] - direction[0]), direction_coord[1], direction_coord[2])
-1 * (self.coordinates[0] - direction[0]), direction_coord[1], direction_coord[2])
if abs(direction_coord[1]) > self.world.get_y_size():
direction_coord = (
direction_coord[0], -1 * (self.coordinates[1] - direction[1]), direction_coord[2])
direction_coord[0], -1 * (self.coordinates[1] - direction[1]), direction_coord[2])
if abs(direction_coord[2]) > self.world.get_z_size():
direction_coord = (
direction_coord[0], direction_coord[1], -1 * (self.coordinates[2] - direction[2]))
direction_coord[0], direction_coord[1], -1 * (self.coordinates[2] - direction[2]))
else:
if abs(direction_coord[0]) > self.world.get_x_size():
direction_coord = (self.coordinates[0], direction_coord[1], direction_coord[2])
......@@ -1079,6 +1079,7 @@ class Particle(matter.Matter):
return False
def set_color(self, color):
super().set_color(color)
if self.world.vis is not None:
self.world.vis.particle_changed(self)
import sys
def get_coordinates_in_direction(coordinates, direction):
"""
Returns the coordinates data of the pointed directions
......@@ -25,18 +23,18 @@ def get_multiple_steps_in_direction(start, direction, steps):
def scan_in(matter_map: dict, center, hop, grid):
result = []
n_sphere_border = grid.get_n_sphere_border(center, hop)
for l in n_sphere_border:
if l in matter_map:
result.append(matter_map[l])
for coords in n_sphere_border:
if coords in matter_map:
result.append(matter_map[coords])
return result
def scan_within(matter_map, center, hop, grid):
result = []
n_sphere_border = grid.get_n_sphere(center, hop)
for l in n_sphere_border:
if l in matter_map:
result.append(matter_map[l])
for coords in n_sphere_border:
if coords in matter_map:
result.append(matter_map[coords])
return result
......@@ -53,4 +51,3 @@ def create_matter_in_line(world, start, direction, amount, matter_type='particle
print("create_matter_in_line: unknown type (allowed: particle, tile or location")
return
current_position = get_coordinates_in_direction(current_position, direction)
......@@ -7,7 +7,7 @@ class Tile(matter.Matter):
"""In the classe marker all the methods for the characterstic of a marker is included"""
def __init__(self, world, coordinates, color):
"""Initializing the marker constructor"""
super().__init__(world, coordinates, color, type="tile", mm_size=world.config_data.tile_mm_size)
super().__init__(world, coordinates, color, matter_type="tile", mm_size=world.config_data.tile_mm_size)
self.__isCarried = False
def get_tile_status(self):
......@@ -31,7 +31,6 @@ class Tile(matter.Matter):
"""
Takes the tile on the given coordinate if it is not taken
:param coordinates: Coordination of tile that should be taken
:return: True: Successful taken; False: Cannot be taken or wrong Coordinates
"""
......
......@@ -4,14 +4,14 @@ from threading import Thread
import cv2
from OpenGL import GL
from PyQt5.QtWidgets import QApplication, QSplitter, QWidget, QFileDialog
from PyQt5.QtWidgets import QApplication, QSplitter, QWidget
from lib.visualization.recorder import Recorder
from lib.visualization.OGLWidget import OGLWidget
import time
from lib.visualization.camera import Camera
from lib.visualization.toms_svg_generator import create_svg
from lib.visualization.utils import LoadingWindow, show_msg
from lib.visualization.utils import LoadingWindow, show_msg, TopQFileDialog, VisualizationError, Level
class ResetException(Exception):
......@@ -94,7 +94,7 @@ class Visualization:
self._viewer.keyPressEventHandler = key_press_event
self._splitter.keyPressEvent = key_press_event
else:
show_msg("No key_handler(key, vis) function found in gui module!", 1)
show_msg("No key_handler(key, vis) function found in gui module!", 1, self._splitter)
# loading gui from gui module
if "create_gui" in dir(self._gui_module):
......@@ -110,11 +110,11 @@ class Visualization:
# noinspection PyUnresolvedReferences
show_msg("The create_gui(world, vis) function in gui module didn't return a QWidget." +
"Expected a QWidget or a subclass, but got %s."
% self._gui.__class__.__name__, 1)
% self._gui.__class__.__name__, 1, self._splitter)
self._splitter.addWidget(self._viewer)
else:
show_msg("No create_gui(world, vis) function found in gui module. GUI not created", 1)
show_msg("No create_gui(world, vis) function found in gui module. GUI not created", 1, self._splitter)
self._splitter.addWidget(self._viewer)
# waiting for the simulation window to be fully active
......@@ -165,7 +165,10 @@ class Visualization:
self._gui.setDisabled(True)
thread.start()
while thread.is_alive():
self._process_events()
try:
self._process_events()
except VisualizationError:
print("jooo")
thread.join()
loading_window.close()
self._gui.setDisabled(False)
......@@ -260,7 +263,30 @@ class Visualization:
def run(self, round_start_timestamp):
"""
main function for running the simulation with the visualization.
Controlls the waiting time, so the rounds_per_second value is being kept.
At this time, its just error handling here.. the simulation and drawing stuff starts in the run_iteration method
:param round_start_timestamp: timestamp of the start of the round.
:return:
"""
try:
self._run_iteration(round_start_timestamp)
except VisualizationError as ve:
if ve.level == Level.INFO:
show_msg(ve.msg, ve.level, self.get_main_window())
if ve.level == Level.CRITICAL:
show_msg(ve.msg, ve.level, self.get_main_window())
exit(1)
if ve.level == Level.WARNING:
try:
self._run_iteration(round_start_timestamp)
show_msg(ve.msg, ve.level, self.get_main_window())
except VisualizationError as ve:
show_msg(ve.msg, ve.level, self.get_main_window())
if ve.level != Level.INFO:
exit(1)
def _run_iteration(self, round_start_timestamp):
"""
Controls the "waiting time", so the rounds_per_second value is being kept at the specified value.
:param round_start_timestamp: timestamp of the start of the round.
:return:
"""
......@@ -273,16 +299,18 @@ class Visualization:
# waiting until simulation starts
self._wait_while_not_running()
# record round
if self._recording:
self.recorder.record_round()
self._splitter.setWindowTitle("Simulator, recorded: %d rounds" % len(self.recorder.records))
# waiting until enough time passed to do the next simulation round.
time_elapsed = time.perf_counter() - round_start_timestamp
# sleeping time - max 1/120 for a responsive GUI
# sleeping time - max 1/120 s for a responsive GUI
sleep_time = min(1.0 / 120, (1.0 / self._rounds_per_second) / 10.0)
max_wait_time = 1 / self._rounds_per_second
while time_elapsed < max_wait_time:
# waiting for 1/100 of the round_time
time.sleep(sleep_time)
# check if still running... if not wait (important for low rounds_per_second values)
self._wait_while_not_running()
......@@ -554,7 +582,7 @@ class Visualization:
def export_recording(self):
if len(self.recorder.records) == 0:
show_msg("No rounds recorded. Nothing to export.", 0)
show_msg("No rounds recorded. Nothing to export.", 0, self._splitter)
return
if self._running:
self.start_stop()
......@@ -564,7 +592,7 @@ class Visualization:
self._gui_module.set_disable_sim(True)
else:
show_msg("No 'set_disable_sim(disable_flag)' function in gui module found."
"\nRunning simulation within recording mode may result in undefined behavior!", 1)
"\nRunning simulation within recording mode may result in undefined behavior!", 1, self._splitter)
self.recorder.show(self.do_export)
......@@ -590,10 +618,9 @@ class Visualization:
directory = "."
if os.path.exists("videos") and os.path.isdir("videos"):
directory = "videos"
path = QFileDialog().getSaveFileName(options=(QFileDialog.Options()),
filter="*.mp4;;*.avi;;*.mkv",
directory=directory)
path = TopQFileDialog(self._splitter).getSaveFileName(options=(TopQFileDialog.Options()),
filter="*.mp4;;*.avi;;*.mkv",
directory=directory)
if path[0] == '':
return
......@@ -603,13 +630,13 @@ class Visualization:
fullpath = path[0] + path[1].replace('*', '')
if animation:
animation_steps = int(30/rps)
animation_steps = int(30 / rps)
if animation_steps < 1:
animation_steps = 1
else:
animation_steps = 1
writer = cv2.VideoWriter(fullpath, cv2.VideoWriter_fourcc(*codec), rps*animation_steps, (width, height))
writer = cv2.VideoWriter(fullpath, cv2.VideoWriter_fourcc(*codec), rps * animation_steps, (width, height))
self._viewer.setDisabled(True)
# creating and opening loading window
lw = LoadingWindow("", "Exporting Video...")
......@@ -619,11 +646,11 @@ class Visualization:
# render and write frame
self._viewer.inject_record_data(self.recorder.records[i])
# animate
for j in range(1, animation_steps+1):
for j in range(1, animation_steps + 1):
# process events so the gui thread does respond to interactions..
self._process_events()
# update loading windows text and progress bar
processing = (i - first_frame_idx + 1)*animation_steps + j
processing = (i - first_frame_idx + 1) * animation_steps + j
lw.set_message("Please wait!\nExporting frame %d/%d..." % (processing, out_of))
lw.set_progress(processing, out_of)
self._viewer.set_animation_percentage(j / animation_steps)
......@@ -636,7 +663,7 @@ class Visualization:
self._viewer.setDisabled(False)
self._viewer.resizeGL(self._viewer.width(), self._viewer.height())
self._viewer.update_scene()
show_msg("Video exported successfully!", 0)
show_msg("Video exported successfully!", 0, self._splitter)
def delete_recording(self):
self.recorder = Recorder(self._world, self._viewer)
......@@ -656,9 +683,9 @@ class Visualization:
if os.path.exists("screenshots") and os.path.isdir("screenshots"):
directory = "screenshots"
path = QFileDialog().getSaveFileName(options=(QFileDialog.Options()),
filter="*.svg",
directory=directory)
path = TopQFileDialog(self._splitter).getSaveFileName(options=(TopQFileDialog.Options()),
filter="*.svg",
directory=directory)
if path[0] == '':
return
......@@ -667,7 +694,7 @@ class Visualization:
else:
create_svg(self._world, path[0] + ".svg")
else:
show_msg("Not implemented yet.\nWorks only with Triangular Grid for now!\nSorry!", 2)
show_msg("Not implemented yet.\nWorks only with Triangular Grid for now!\nSorry!", 1, self._splitter)
def set_animation(self, animation):
if not animation:
......
......@@ -2,9 +2,8 @@ import OpenGL.GL as GL
from PIL import Image
from PyQt5 import QtOpenGL, QtGui, QtCore
from PyQt5.QtGui import QOpenGLFramebufferObject, QOpenGLFramebufferObjectFormat
from PyQt5.QtWidgets import QFileDialog
from lib.visualization.utils import MatterInfoFrame
from lib.visualization.utils import MatterInfoFrame, TopQFileDialog
from lib.visualization.programs.offset_color_carry_program import OffsetColorCarryProgram
from lib.visualization.programs.offset_color_program import OffsetColorProgram
from lib.visualization.programs.grid_program import GridProgram
......@@ -13,7 +12,7 @@ import cv2
import os
import datetime
from lib.visualization.utils import show_msg
from lib.visualization.utils import VisualizationError, Level
class OGLWidget(QtOpenGL.QGLWidget):
......@@ -510,26 +509,27 @@ class OGLWidget(QtOpenGL.QGLWidget):