Skip to content
Snippets Groups Projects
Commit c8c85abb authored by Jonas Kvarnström's avatar Jonas Kvarnström
Browse files

Merge branch 'devel2021' into 'master'

Emil: added the visualdebbuggers and a tictoc to make checking of time easy

See merge request !9
parents b89bb5a5 804e5556
No related branches found
No related tags found
1 merge request!9Emil: added the visualdebbuggers and a tictoc to make checking of time easy
import time
class TicToc:
"""
Class to make the checking of how long times different parts of the program takes. Simply
write tic() where you want to start the timer and toc() when you want to stop timer. Can handle
multiple timers simultaneously by sending a key to tic and toc. Reset everything with reset()
and a specific key by providing thus key
"""
def __init__(self):
self.logs = None
self._time_start = {"main": 0.0}
self.reset()
def __repr__(self):
"""
How the class should be printed, will print like a normal dictionary
:return: Normal representation of a dictionary
"""
return self.logs.__repr__()
def __getitem__(self, item):
"""
Getter to retrieve a single timer
:param item: name of the log you want get
:return: Timer of the provided name
"""
return self.logs.get(item)
def __iter__(self):
"""
Iterator for the times
:return: iterator for the times that are logged
"""
return self.logs.items().__iter__()
def tic(self, key="main"):
"""
Start the timer for provided key
:param key: Name of the timer, used to save start time in a dict, defaults to "main"
:return: None
"""
if key not in self.logs:
self.logs[key] = 0.0
self._time_start[key] = time.time()
def toc(self, key="main"):
"""
Saves the time for provided key. Can be used multiple times from the same tic() then it will do addition
:param key: Name of the timer defaults to "main"
:return: None
"""
try:
self.logs[key] += time.time() - self._time_start[key]
except KeyError:
"The provided key has not been initialized, need to run tic() before running toc()"
def reset(self, key=None):
"""
Reset the log for the given key if the key exists. Leave key empty to reset all logs
:param key: what key you want to reset, defaults to None which will reset everything
:return: None
"""
if key is None:
self.logs = {"main": 0.0}
elif key in self.logs:
self.logs[key] = 0.0
# Visual Debugging
These files provide some useful and hopefully easy to use tools to visualize what data you currently have.
The debuggers are using pygame to open a new window and draw shapes onto it. Feel free to modify the debugger in
order to suit your needs.
Also Note that since pygame only can have one window open at a time you are NOT able to run two different
types of debuggers simultaneous.
## Getting started
In the example below you can see how you get started with the visual debugger. In this example a debugger for a heatmap is set as the debugger.
```python
from library import *
import numpy as np
import visualdebugger.heat_map_debugger
import visualdebugger.flow_debugger
import visualdebugger.path_debugger
class MyAgent(IDABot):
def __init__(self):
IDABot.__init__(self)
self.debugging = True
self.debugger = None
if self.debugging:
self.set_up_debugging()
def set_up_debugging(self):
self.debugger = visualdebugger.heat_map_debugger.HeatMapDebugger()
self.debugger.set_color_map({(20, 20): (255, 255, 0)})
self.debugger.set_update_frequency(0.5)
def on_game_start(self):
IDABot.on_game_start(self)
self.debugger.on_start()
def on_step(self):
IDABot.on_step(self)
if self.debugging:
self.debugger.on_step(self.calculate_and_set_heatmap) # OBS function that is sent! NOT function call!
def calculate_and_set_heatmap(self):
my_units = self.get_my_units()
heat = np.zeros((self.my_bot.map_tools.height, self.my_bot.map_tools.width))
for unit in my_units:
heat[int(unit.position.y)][int(unit.position.x)] = 20
self.debugger.set_display_values(heat) # OBS important to set the values
```
1. You are probably best off having a boolean variable to make the switch between having the debugger on or off easy.
2. Initialize the type of visual debugger you want to use by calling the constructor for that class. In the example
above this is done in the first line of “set_up_debugging()” and this function.
3. The debugger must set up a window in order to have somewhere to draw onto. This is done in its on_start(). A good
place to call on this function is in on_game_start() just like in the example above.
4. In order to update what the debugger shows you call its on_step function. This function will only do things as
often as the update frequency is set to (in the example above it is set to 0.5 Hz meaning once every other second).
So you can call this in the agent on_step() just like in the example above. This on step function can also take another
function and call on that function at that same frequency. It can be a good idea to have a function that calls the
.set_display_values() and have that being called this way.
Note: The .set_display_values() takes different arguments based on what type of debugger you are using and if the
values ain’t correctly formatted a print will occur and say so. Please continue reading this in order to see how to
format the values or take a look directly in the source code.
## Structure
In this part you can see how to structure the parameters in order to show the values.
### All VisualDebuggers:
##### on_start(self)
Sets up a pygame window that the on_step will draw onto.
##### on_step(self, fun=None)
Will keep track of the time that the last update of the window occured and if it should update it will do so. And will
also call on the function provided at the same time.
##### set_update_frequency(self, new_frequency)
Set how often you want the on_step to do things. The frequency is in Hertz (updates/second)
##### set_display_values(self, *args)
Set the values that should be displayed. The different types of debuggers have a bit different criterias for their
arguments so please check below or check the source code to get a better understanding.
##### set_color_map(self, color_map)
The Heatmap and Path debuggers make it easy to have different colors being displayed based on value. Use this function
to do so by inputting a color_map. The color_map should be a dictionary with tuples of length 2 as keys and length 3
as values. The key is the interval of which the color should represent. The interval will include the first value and
exclude the last value. And the three long tuples are (r, g, b) values. Note that if the colormap is not correctly
formatted this call won't do anything and print that there was something wrong. Intervals are not allowed to overlap!
### FlowDebugger
##### .set_display_values(self, map_values)
map_values should be a 2d list or 2d numpy array where every row has the same length. The values have to be integers
or floats between 0 and 2*pi. The values represent the angle in radians and when run, the window will display arrows
pointing in those directions.
### HeatMapDebugger
##### .set_display_values(self, map_values)
map_values should be a 2d list or 2d numpy array where every row has the same length. The values have to be integers
or floats. You are free to have these values represent whatever you want and based on the color_map you have set, the
color of each square will have that color.
### PathDebugger
##### .set_display_values(self, relevant_coordinates, map_size=(152, 176))
This debugger can more or less do the exact same thing as the heatmap debugger but instead of having to provide an
entire 2d array you just need to provide the specific coordinates as the relevant_coordinates parameter. This should
be a dictionary with a tuple of coordinates (x, y) both integer values as key and the value you want that coordinate
to have is the value in the dictionary. You can also provide the size of the map you want as a second tuple. This size
defaults to (152, 176) since that is the size of the normal map used. But keep in mind that all the coordinates must
be in the range of the map_size!
When using this debugger you can just like with the heatmap set the colormap in order to have different values for
different coordinates display different colors.
import visualdebugger.visual_debugger
import numpy as np
import pygame
import math
class FlowDebugger(visualdebugger.visual_debugger.VisualDebugger):
def __init__(self):
super(FlowDebugger, self).__init__()
self.direction_map = []
self.arrows = []
self.arrow_color = (255, 0, 0)
def on_draw(self):
self.screen.fill(self.background_color)
for arrow in self.arrows:
self.screen.blit(pygame.transform.rotate(arrow[0], math.degrees(arrow[2])), arrow[1])
pygame.display.flip()
self.screen.fill(self.background_color)
def on_step(self, fun=None):
super().on_step(fun)
if self.should_update:
self.private_on_step()
def private_on_step(self):
if not self.screen:
self.on_start()
else:
pygame.event.pump()
self.arrows = []
# create squares and bind them to a value
for y_pos in range(len(self.direction_map)):
for x_pos in range(len(self.direction_map[y_pos])):
tile_width = int((self.screen.get_width() -
len(self.direction_map[y_pos]) * self.tile_margin) / len(self.direction_map[y_pos]))
tile_height = int((self.screen.get_height() -
len(self.direction_map) * self.tile_margin) / len(self.direction_map))
surface = pygame.Surface((tile_width, tile_height))
pygame.draw.polygon(surface, self.arrow_color, ((0+tile_width/6, tile_height/3),
(0+tile_width/6, 2*tile_height/3),
(2*tile_width/4, 2*tile_height/3),
(2*tile_width/4, 5*tile_height/6),
(5*tile_width/6, tile_height/2),
(2*tile_width/4, 0+tile_height/6),
(2*tile_width/4, tile_height/3))
)
dest_x = x_pos * (tile_width + self.tile_margin) + self.tile_margin
# y is flipped in order to match sc2 coords
dest_y = (len(self.direction_map) - y_pos - 1) * (tile_height + self.tile_margin) + self.tile_margin
position = (dest_x, dest_y)
self.arrows.append((surface, position, self.direction_map[y_pos][x_pos]))
self.on_draw()
def set_display_values(self, map_values):
"""
Set what map that should be displayed, it has to be a 2d list
or a 2d numpy array. Where every row in the list has the same length.
The values has to be ints or floats between 0 and 2*pi
:param map_values: [[], []. [], ...]
:return: none, sets the values to display
"""
try:
assert isinstance(map_values, (list, np.ndarray)) and len(map_values)
first_row = map_values[0]
for row in map_values:
assert isinstance(row, (list, np.ndarray)) and len(row)
assert(len(first_row) == len(row))
for column in row:
assert isinstance(column, (int, float))
assert 0 <= column <= 2*math.pi
self.direction_map = map_values
except AssertionError:
print("The map you set for debugger must be a list or numpy.array. It cant be empty and "
"every row has to have the same length")
import visualdebugger.visual_debugger
import numpy as np
import pygame
class HeatMapDebugger(visualdebugger.visual_debugger.VisualDebugger):
def __init__(self):
super(HeatMapDebugger, self).__init__()
self.map_to_display = []
self.color_map = {}
self.squares = []
def on_draw(self):
self.screen.fill(self.background_color)
for square in self.squares:
pygame.draw.rect(self.screen, self.get_color_of_tile(square[1]), square[0])
pygame.display.flip()
self.screen.fill(self.background_color)
def on_step(self, fun=None):
super().on_step(fun)
if self.should_update:
self.private_on_step()
def private_on_step(self):
if not self.screen:
self.on_start()
else:
pygame.event.pump()
self.squares = []
# create squares and bind them to a value
for y_pos in range(len(self.map_to_display)):
for x_pos in range(len(self.map_to_display[y_pos])):
tile_val = self.map_to_display[y_pos][x_pos]
tile_width = int((self.screen.get_width() - len(self.map_to_display[y_pos]) * self.tile_margin)
/ len(self.map_to_display[y_pos]))
tile_height = int((self.screen.get_height() - len(self.map_to_display) * self.tile_margin)
/ len(self.map_to_display))
rect_pos_x = x_pos * (tile_width + self.tile_margin) + self.tile_margin
rect_pos_y = (len(self.map_to_display) - y_pos - 1) * \
(tile_height + self.tile_margin) + self.tile_margin
self.squares.append((pygame.Rect(rect_pos_x, rect_pos_y, tile_width, tile_height),
tile_val))
self.on_draw()
def set_display_values(self, map_values):
"""
Set what map that should be displayed, it has to be a 2d list
or a 2d numpy array. Where every row in the list has the same length
:param map_values: [[], []. [], ...]
:return:
"""
try:
assert isinstance(map_values, (list, np.ndarray)) and len(map_values)
first_row = map_values[0]
for row in map_values:
assert isinstance(row, (list, np.ndarray)) and len(row)
assert(len(first_row) == len(row))
for number in row:
assert isinstance(number, (int, float))
self.map_to_display = map_values
except AssertionError:
print("The map you set for debugger must be a list or numpy.array. It cant be empty and "
"every row has to have the same length and every element must be a int or float")
def set_color_map(self, color_map):
"""
Setter for the colormap. takes a dict with tuple of interval as key and rgb value it should represent as value.
interval is (include, exclude).
:param color_map: {interval(include, exclude): color (r, g, b)}.
{(start of interval included, end of interval excluded): (r, g, b)}
:return: None, sets the colormap of the Heatmap
"""
try:
assert isinstance(color_map, dict) and len(color_map)
occupied_intervals = []
for key in color_map.keys():
assert isinstance(key, tuple) and isinstance(color_map[key], tuple) and \
len(key) == 2 and len(color_map[key]) == 3
assert key[0] <= key[1] and isinstance(key[0], (int, float)) \
and isinstance(key[1], (int, float))
for interval in occupied_intervals:
if not (key[1] <= interval[0] or key[0] >= interval[1]):
print("interval: " + str(key) + " overlapped with: " + str(interval))
raise ValueError
occupied_intervals.append(key)
for color_val in color_map[key]:
assert 0 <= color_val <= 255
self.color_map = color_map
except AssertionError:
print("The colormap provided was not correctly formatted")
except ValueError:
print("The colormap provided had overlapping intervals")
def get_color_of_tile(self, tile_value):
for interval in self.color_map.keys():
if interval[0] <= tile_value < interval[1] or interval[0] == tile_value:
return self.color_map[interval]
return 0, 0, 0
import visualdebugger.visual_debugger
import numpy as np
import pygame
class PathDebugger(visualdebugger.visual_debugger.VisualDebugger):
def __init__(self):
super(PathDebugger, self).__init__()
self.map_to_display = []
self.color_map = {}
self.squares = []
def on_draw(self):
self.screen.fill(self.background_color)
for square in self.squares:
pygame.draw.rect(self.screen, self.get_color_of_tile(square[1]), square[0])
pygame.display.flip()
self.screen.fill(self.background_color)
def on_step(self, fun=None):
super().on_step(fun)
if self.should_update:
self.private_on_step()
def private_on_step(self):
if not self.screen:
self.on_start() # (super)
else:
pygame.event.pump()
self.squares = []
# create squares and bind them to a value
for y_pos in range(len(self.map_to_display)):
for x_pos in range(len(self.map_to_display[y_pos])):
tile_val = self.map_to_display[y_pos][x_pos]
tile_width = int((self.screen.get_width() - len(self.map_to_display[y_pos]) * self.tile_margin)
/ len(self.map_to_display[y_pos]))
tile_height = int((self.screen.get_height() - len(self.map_to_display) * self.tile_margin)
/ len(self.map_to_display))
rect_pos_x = x_pos * (tile_width + self.tile_margin) + self.tile_margin
rect_pos_y = (len(self.map_to_display) - y_pos - 1) * \
(tile_height + self.tile_margin) + self.tile_margin
self.squares.append((pygame.Rect(rect_pos_x, rect_pos_y, tile_width, tile_height),
tile_val))
self.on_draw()
def set_display_values(self, relevant_coordinates, map_size=(152, 176)):
"""
Set what map that should be displayed, it has to be a 2d list
or a 2d numpy array. Where every row in the list has the same length
:param relevant_coordinates: {(x, y) : value, (x, y): value, (x, y): value, ...}
:param map_size: (x:int, y:int)=(152, 176) size of map. relevant_coordinates.keys() must be in the range of
these values
:return:
"""
try:
assert isinstance(relevant_coordinates, dict) and len(relevant_coordinates)
assert (isinstance(map_size, tuple) and len(map_size) == 2)
assert isinstance(map_size[0], int) and isinstance(map_size[1], int) and \
map_size[0] > 0 and map_size[1] > 0 # check that map size is valid
for coord in relevant_coordinates.keys():
assert isinstance(coord, tuple) and len(coord) == 2 and \
isinstance(coord[0], int) and isinstance(coord[1], int)
assert (0 <= coord[0] < map_size[0] and 0 <= coord[1] < map_size[1])
assert isinstance(relevant_coordinates[coord], (int, float))
# set the map_to_display value of the relevant coords to the value provided. everything else zero.
self.map_to_display = np.zeros((map_size[1], map_size[0]))
for coord in relevant_coordinates.keys():
self.map_to_display[coord[1]][coord[0]] = relevant_coordinates[coord]
except AssertionError:
print("The relevant coordinates you set must be a dictionary with a tuple as key (x_pos, y_pos) \n"
"and int or float as value. Coordinates must be in the range of the 0 to map_size-1 and \n"
"the map_size must be a tuple with (width, height) map_size defaults to (152, 176)")
def set_color_map(self, color_map):
"""
Setter for the colormap. takes a dict with tuple of interval as key and rgb value it should
represent as value. interval is (include, exclude).
:param color_map: {interval(include, exclude): color (r, g, b)}.
{(start of interval included, end of interval excluded): (r, g, b)}
:return: None, sets the colormap of the Heatmap
"""
try:
assert isinstance(color_map, dict) and len(color_map)
occupied_intervals = []
for key in color_map.keys():
assert isinstance(key, tuple) and isinstance(color_map[key], tuple) and \
len(key) == 2 and len(color_map[key]) == 3
assert isinstance(key[0], (int, float)) and isinstance(key[1], (int, float)) and key[0] <= key[1]
for interval in occupied_intervals:
if not (key[1] <= interval[0] or key[0] >= interval[1]):
print("interval: " + str(key) + " overlapped with: " + str(interval))
raise ValueError
occupied_intervals.append(key)
for color_val in color_map[key]:
assert isinstance(color_val, int)
assert 0 <= color_val <= 255
self.color_map = color_map
except AssertionError:
print("The colormap provided was not correctly formatted")
except ValueError:
print("The colormap provided had overlapping intervals")
def get_color_of_tile(self, tile_value):
for interval in self.color_map.keys():
if interval[0] <= tile_value < interval[1] or interval[0] == tile_value:
return self.color_map[interval]
return 0, 0, 0
import time
import pygame
class VisualDebugger:
"""
Basic class for a visual debugger, easy to take this and make
a specific type of visual debugger a subclass of this.
Look at the other debuggers if you need guidance.
"""
def __init__(self):
self.default_width = 152*5
self.default_height = 176*5
self.tile_margin = 1
self.background_color = (0, 0, 0)
self.screen = None
self.update_frequency = 0.5 # update frequency in Hz (updates per second)
self.last_update = 0
self.should_update = True
def on_start(self):
self.screen = pygame.display.set_mode((self.default_width, self.default_height))
def on_step(self, fun=None):
if time.time() - self.last_update > 1/self.update_frequency: # updates if enough time has passed
if callable(fun):
fun()
self.last_update = time.time()
self.should_update = True
else:
self.should_update = False
def set_update_frequency(self, new_frequency):
self.update_frequency = new_frequency
def set_tile_margin(self, margin):
self.tile_margin = margin
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment