# Karma Game Library, Copyrights Kevin Riehl 2023, <kriehl@ethz.ch>
# Imports
import numpy as np
from sklearn.preprocessing import normalize
import PIL.Image
import matplotlib.pyplot as plt
import numpy.typing as npt
from typing import Set, List, Iterable
# Functions
def _convert_set_to_labels(set_instance: Set):
"""
This function converts a set to a list of string.
Parameters
----------
set_instance : Set
The set to be converted.
Returns
-------
list_return : List[str]
The list of strings generated from the set.
"""
return [str(x) for x in set_instance]
[docs]def draw_distribution_bar(set_karmas: Set[int], values: Iterable[float], std_errs: Iterable[float]=None, bins:int=None):
"""
This function draws a bar chart from a given vector of values using
matplotlib.pyplot.
Parameters
----------
set_karmas : Set[int]
The set_policy_karmas.
values : Iterable[float]
The values to plot.
std_errs: Iterable[float]
Optional parameter, representing standard errors.
bins: int
Optional parameter, Default: None. If provided, only 'bins' ticks are
displayed on the x axis.
Returns
-------
None
"""
labels = _convert_set_to_labels(set_karmas)
plt.bar(labels, values, yerr=std_errs, tick_label=labels)
plt.setp(plt.gca().get_xticklabels(), rotation=90)
if bins is not None:
plt.locator_params(axis='x', nbins=bins)
[docs]def draw_heatmap(matrix: npt.NDArray, labels_x: List[str], labels_y: List[str], cmap:str="Blues", bins:int=None):
"""
This function draws a heatmap from a given matrix of values using
matplotlib.pyplot.
Parameters
----------
matrix : npt.NDArray
The matrix to be drawn.
labels_x : List[str]
The list of labels for the abscissa.
labels_y : List[str]
The list of labels for the ordinate.
cmap: str
Optional parameter, representing the colormap.
Default is 'Blues'.
Returns
-------
None
"""
plt.imshow(matrix, cmap=cmap, interpolation="nearest", origin='lower', extent=[0,1,0,1])
n = len(labels_x)
plt.gca().set_xticks([1/n*(i+1) - (1/(2*n)) for i in range(0,n)])
plt.gca().set_xticklabels(labels_x)
plt.setp(plt.gca().get_xticklabels(), rotation=90)
if bins is not None:
plt.locator_params(axis='x', nbins=bins)
n = len(labels_y)
plt.gca().set_yticks([1/n*(i+1) - (1/(2*n)) for i in range(0,n)])
plt.gca().set_yticklabels(labels_y)
[docs]def draw_specific_policy(policy, game_parameters, atype, urgency, bins:int=None):
"""
This function draws the probability of certain actions according to
karma balances as heatmap for a specific type and urgency using
matplotlib.pyplot.
Parameters
----------
policy : Policy
The policy used in simulation.
game_parameters : GameParameters
atype : int
The specific agent type.
urgency : int
The specific agent urgency.
bins: int
Optional parameter, Default: None. If provided, only 'bins' ticks are
displayed on the x axis.
Returns
-------
None
"""
karma_action_probabilities = policy.prob_matrix[atype][urgency]
labels_x = _convert_set_to_labels(game_parameters._set_state_karmas)
labels_y = _convert_set_to_labels(game_parameters._set_actions)
draw_heatmap(karma_action_probabilities.transpose(), labels_x, labels_y, bins=bins)
plt.xlabel("Karma balance")
plt.ylabel("Action")
[docs]def draw_karma_distribution_from_state(state, game_parameters, limit:int=None, atype: int=None, urgency:int=None, bins:int=None):
"""
This function draw the Karma distirbution from a given state distribution
for a specific agent type. The karma distribution shows the average across
all urgencies, if the urgency level is unspecified (None).
Parameters
----------
state : StateDistribution
The state distribution to draw from.
game_parameters : GameParameters
The game parameters used.
limit : int
Optional. Default is None. If not defined, the full distribution is
displayed, otherwise only until the limit. The last bar will then
represent the karma balances greater or equal that value.
atype : int
Optional. Default is None. If not defined, the average across all
types is displayed, otherwise for the agent type specifically.
urgency : int
Optional. Default is None. If not defined, the average across all
urgencies is displayed, otherwise for the urgency level specifically.
bins: int
Optional parameter, Default: None. If provided, only 'bins' ticks are
displayed on the x axis.
Returns
-------
None
"""
if limit is None:
labels_to_draw = game_parameters._set_state_karmas
matrix_to_draw = state.dist_matrix
else:
labels_to_draw = np.arange(0,limit+1).tolist()
matrix_to_draw = state.get_limited_dist_matrix(limit=limit+1)
if atype is None:
matrix_to_draw = np.mean(matrix_to_draw, axis=0)
else:
matrix_to_draw = matrix_to_draw[atype]
if urgency is None:
matrix_to_draw = np.mean(matrix_to_draw, axis=0)
else:
matrix_to_draw = matrix_to_draw[urgency]
draw_distribution_bar(labels_to_draw, matrix_to_draw, bins=bins)
plt.xlabel("Karma balance")
plt.ylabel("Share of population")
[docs]def draw_karma_distribution_from_simulator(simulator, game_parameters, bins:int=None):
"""
This function draw the Karma distribution from a given state distribution
for a specific agent type. The karma distribution shows the average across
all urgencies, if the urgency level is unspecified (None).
Parameters
----------
simulator : Simulator
The state distribution to draw from.
game_parameters : GameParameters
The game parameters used.
bins: int
Optional parameter, Default: None. If provided, only 'bins' ticks are
displayed on the x axis.
Returns
-------
None
"""
distribution = simulator.get_karma_distribution()
distribution = distribution / len(distribution)
draw_distribution_bar(np.arange(0, len(distribution)), distribution, bins=bins)
plt.xlabel("Karma balance")
plt.ylabel("Share of population")
[docs]def draw_distribution_from_simulator(simulator, game_parameters, column: int, mode: str, bins:int=15):
"""
This function draw the interaction distribution from a given state
distribution for a specific agent type. The karma distribution shows the
average across all urgencies, if the urgency level is unspecified (None).
Parameters
----------
simulator : Simulator
The state distribution to draw from.
game_parameters : GameParameters
The game parameters used.
column : int
The column from the agent population. Each column is represented by
following enumeration: TYPE_COL = 0, URGENCY_COL = 1, KARMA_COL = 2,
CUM_COST_COL = 3, ENCOUNTERS_COL = 4.
mode : str
Which mode for the distribution. Possible options are: 'unique' to
display how often all unique elements of the population occur;
'histogram' to display how often values occur in predefined bins.
bins : int
Optional. Default=15. This defines how many bins are used for the mode
'histogram'.
Returns
-------
None
"""
population = simulator._population[:, column]
if mode=='unique':
vals, counts = np.unique(population, return_counts=True)
elif mode=='histogram':
counts, vals = np.histogram(population, bins=bins)
vals = vals[:-1]
vals = [f'{x:.3f}' for x in vals]
draw_distribution_bar(vals, counts)
plt.ylabel("Occurences in population")
[docs]def draw_karma_transition_heatmap_from_simulator(simulator, game_parameters):
"""
This function draws a karma transition heatmap from a given state
transition matrix of a simulation as average over types and urgencies
using matplotlib.pyplot.
Parameters
----------
simulator : Simulator
The state_transition_distribution from a KarmaSimulationInstance.
game_parameters : The game parameters
The set_state_karmas.
Returns
-------
None
"""
mean_over_urgencies = simulator.get_state_transition_counts()
mean_over_urgencies = np.sum(mean_over_urgencies, axis=0)
mean_over_urgencies = np.sum(mean_over_urgencies, axis=1)
mean_over_urgencies = normalize(mean_over_urgencies, axis=0, norm="l1")
labels = _convert_set_to_labels(game_parameters._set_state_karmas)
draw_heatmap(mean_over_urgencies, labels, labels)
plt.xlabel("Karma before")
plt.ylabel("Karma after")
[docs]def render_gif_animation(lst_image_files: List[str], target_file: str, speed:int=100, first_last_slow:bool=True):
"""
This function reads static images from a list of image files, and stores
all of them in an GIF animation.
Parameters
----------
lst_image_files: List[str]
A list with image files that shall be connected to a GIF animation.
target_file : str
The target file to store the GIF into.
speed : int
Optional, Defualt: 100. The time per image. The slower the faster the animation.
first_last_slow : bool
Optional, Default : True. This will repeat the first and the last
image for ten times, so the animation does not directly run.
Returns
-------
None
"""
frames = []
if first_last_slow:
for x in range(0,10):
image = PIL.Image.open(lst_image_files[0])
frames.append(image)
for file in lst_image_files:
image = PIL.Image.open(file)
frames.append(image)
if first_last_slow:
for x in range(0,10):
frames.append(frames[-1])
frames[0].save(target_file, format='GIF',
append_images=frames[1:],
save_all=True,
duration=speed, loop=0)