Source code for egttools.plotting.simplified

# Copyright (c) 2019-2022  Elias Fernandez
#
# This file is part of EGTtools.
#
# EGTtools is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# EGTtools is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with EGTtools.  If not, see <http://www.gnu.org/licenses/>

"""Simplified plotting functions"""
import matplotlib.pyplot as plt
import numpy as np

from typing import Optional, Tuple, Callable, List
from ..games import (AbstractGame, Matrix2PlayerGameHolder, MatrixNPlayerGameHolder, )
from .. import (calculate_nb_states, )
from .helpers import (barycentric_to_xy_coordinates,
                      xy_to_barycentric_coordinates, calculate_stability,
                      find_roots_in_discrete_barycentric_coordinates)
from ..analytical import (replicator_equation, PairwiseComparison, )
from ..analytical import replicator_equation_n_player
from ..analytical.utils import check_if_there_is_random_drift, check_replicator_stability_pairwise_games, find_roots
from ..helpers.vectorized import (vectorized_replicator_equation, vectorized_replicator_equation_n_player,
                                  vectorized_barycentric_to_xy_coordinates)
from . import Simplex2D


[docs]def plot_replicator_dynamics_in_simplex(payoff_matrix: np.ndarray, group_size: int = 2, nb_points_simplex: int = 100, nb_of_initial_points_for_root_search: int = 10, atol: float = 1e-7, atol_equal: float = 1e-12, method_find_roots: str = 'hybr', atol_stability_pos: float = 1e-4, atol_stability_neg: float = 1e-4, atol_stability_zero: float = 1e-4, figsize: Tuple[int, int] = (10, 8), ax: Optional[plt.axis] = None) -> Tuple[Simplex2D, Callable[[np.ndarray, int], np.ndarray], List[np.ndarray], List[np.ndarray], List[int]]: """ Helper function to simplify the plotting of the replicator dynamics in a 2 Simplex. Parameters ---------- payoff_matrix: numpy.ndarray The square payoff matrix. Group games are still unsupported in the replicator dynamics. This feature will soon be added. group_size: int size of the group nb_points_simplex: int Number of initial points to draw in the simplex nb_of_initial_points_for_root_search: int Number of initial points used in the method that searches for the roots of the replicator equation atol: float Tolerance to consider a value equal to zero. This is used to check if an edge has random drift. By default, the tolerance is 1e-7. atol_equal: float Tolerance to consider two arrays equal. method_find_roots: str Method used in scipy.optimize.root atol_stability_neg: float Tolerance used to determine the stability of the roots. This is used to determine whether an eigenvalue is negative. atol_stability_pos: float Tolerance used to determine the stability of the roots. This is used to determine whether an eigenvalue is positive. atol_stability_zero: float Tolerance used to determine the stability of the roots. This is used to determine whether an eigenvalue is zero. figsize: Tuple[int, int] Size of the figure. This parameter is only used if the ax parameter is not defined. ax: Optional[matplotlib.pyplot.axis] A matplotlib figure axis. Returns ------- Tuple[Simplex2D, Callable[[numpy.ndarray, int], numpy.ndarray], List[numpy.ndarray], List[numpy.ndarray], List[int]] A tuple with the simplex object which can be used to add more features to the plot, the function that can be used to calculate gradients and should be passed to `Simplex2D.draw_trajectory_from_roots` and `Simplex2D.draw_scatter_shadow`, a list of the roots in barycentric coordinates, a list of the roots in cartesian coordinates and a list of booleans or integers indicating whether the roots are stable. """ if (group_size > 2) and (payoff_matrix.shape[1] == payoff_matrix.shape[0]): nb_group_configurations = calculate_nb_states(group_size, payoff_matrix.shape[0]) if payoff_matrix.shape[1] != nb_group_configurations: raise ValueError("The number of columns of the payoff matrix must be equal " "the the number of possible group configurations, when group_size > 2.") simplex = Simplex2D(nb_points=nb_points_simplex) simplex.add_axis(figsize, ax) random_drift = check_if_there_is_random_drift(payoff_matrix, group_size=group_size, atol=atol) simplex.add_edges_with_random_drift(random_drift) v = np.asarray(xy_to_barycentric_coordinates(simplex.X, simplex.Y, simplex.corners)) if group_size > 2: results = vectorized_replicator_equation_n_player(v, payoff_matrix, group_size) def gradient_function(u): return replicator_equation_n_player(u, payoff_matrix, group_size) else: results = vectorized_replicator_equation(v, payoff_matrix) def gradient_function(u): return replicator_equation(u, payoff_matrix) xy_results = vectorized_barycentric_to_xy_coordinates(results, simplex.corners) ux = xy_results[:, :, 0].astype(np.float64) uy = xy_results[:, :, 1].astype(np.float64) simplex.apply_simplex_boundaries_to_gradients(ux, uy) roots = find_roots(gradient_function=gradient_function, nb_strategies=payoff_matrix.shape[0], nb_initial_random_points=nb_of_initial_points_for_root_search, atol=atol_equal, tol_close_points=atol_equal, method=method_find_roots) roots_xy = [barycentric_to_xy_coordinates(root, corners=simplex.corners) for root in roots] if group_size > 2: stability = calculate_stability(roots, gradient_function) stability = [1 if x is True else -1 for x in stability] else: stability = check_replicator_stability_pairwise_games(roots, payoff_matrix, atol_neg=atol_stability_neg, atol_pos=atol_stability_pos, atol_zero=atol_stability_zero) return simplex, lambda u, t: replicator_equation(u, payoff_matrix), roots, roots_xy, stability
[docs]def plot_pairwise_comparison_rule_dynamics_in_simplex(population_size: int, beta: float, payoff_matrix: np.ndarray = None, game: AbstractGame = None, group_size: Optional[int] = 2, atol: Optional[float] = 1e-7, figsize: Optional[Tuple[int, int]] = (10, 8), ax: Optional[plt.axis] = None) -> \ Tuple[Simplex2D, Callable[[np.ndarray, int], np.ndarray], List[np.ndarray], List[np.ndarray], List[ bool], AbstractGame, PairwiseComparison]: """ Helper function to simplify the plotting of the moran dynamics in a 2 Simplex. Parameters ---------- population_size: Size of the finite population. beta: Intensity of selection. payoff_matrix: The square payoff matrix. Group games are still unsupported in the replicator dynamics. This feature will soon be added. game: Game that should contain a set of payoff matrices group_size: Size of the group. By default, we assume that interactions are pairwise (the group size is 2). atol: Tolerance to consider a value equal to zero. This is used to check if an edge has random drift. By default the tolerance is 1e-7. figsize: Size of the figure. This parameter is only used if the ax parameter is not defined. ax: A matplotlib figure axis. Returns ------- A tuple with the simplex object which can be used to add more features to the plot, the function that can be used to calculate gradients and should be passed to `Simplex2D.draw_trajectory_from_roots` and `Simplex2D.draw_scatter_shadow`, a list of the roots in barycentric coordinates, a list of the roots in cartesian coordinates and a list of booleans indicating whether the roots are stable. It also returns the game class (this is important, since a new game is created when passing a payoff matrix, and if not returned, a reference to the game instance will disappear, and it will produce a segmentation fault). Finally, it also returns a reference to the evolver object. """ if (payoff_matrix is None) and (game is None): raise Exception("You need to define either a payoff matrix or a game.") elif game is None: if (group_size is None) or (group_size < 2): raise Exception("group_size not be None and must be >= 2") if group_size == 2: game = Matrix2PlayerGameHolder(payoff_matrix.shape[0], payoff_matrix) else: game = MatrixNPlayerGameHolder(payoff_matrix.shape[0], group_size, payoff_matrix) simplex = Simplex2D(discrete=True, size=population_size, nb_points=population_size + 1) simplex.add_axis(figsize, ax) random_drift = check_if_there_is_random_drift(payoff_matrix=game.payoffs(), population_size=population_size, group_size=group_size, beta=beta, atol=atol) simplex.add_edges_with_random_drift(random_drift) v = np.asarray(xy_to_barycentric_coordinates(simplex.X, simplex.Y, simplex.corners)) v_int = np.floor(v * population_size).astype(np.int64) evolver = PairwiseComparison(population_size=population_size, game=game) # evolver = StochDynamics(nb_strategies=3, payoffs=payoff_matrix, pop_size=population_size, group_size=group_size) result = np.zeros(shape=(v_int.shape[1], v_int.shape[2], 3)) for i in range(v_int.shape[1]): for j in range(v_int.shape[2]): result[i, j, :] = evolver.calculate_gradient_of_selection(beta, v_int[:, i, j]) result = result.swapaxes(0, 1).swapaxes(0, 2) xy_results = vectorized_barycentric_to_xy_coordinates(result, simplex.corners) ux = xy_results[:, :, 0].astype(np.float64) uy = xy_results[:, :, 1].astype(np.float64) simplex.apply_simplex_boundaries_to_gradients(ux, uy) roots = find_roots_in_discrete_barycentric_coordinates( lambda u: population_size * evolver.calculate_gradient_of_selection(beta, u), population_size, nb_interior_points=calculate_nb_states(population_size, 3), atol=1e-1) roots_xy = [barycentric_to_xy_coordinates(x, simplex.corners) for x in roots] stability = calculate_stability(roots, lambda u: population_size * evolver.calculate_gradient_of_selection(beta, u)) return (simplex, lambda u, t: population_size * evolver.calculate_gradient_of_selection(beta, u), roots, roots_xy, stability, game, evolver)
[docs]def plot_pairwise_comparison_rule_dynamics_in_simplex_without_roots(population_size: int, beta: float, payoff_matrix: np.ndarray = None, game: AbstractGame = None, group_size: Optional[int] = 2, figsize: Optional[Tuple[int, int]] = (10, 8), ax: Optional[plt.axis] = None) -> \ Tuple[Simplex2D, Callable[[np.ndarray, int], np.ndarray], AbstractGame, PairwiseComparison]: """ Helper function to simplify the plotting of the moran dynamics in a 2 Simplex. Parameters ---------- population_size: Size of the finite population. beta: Intensity of selection. payoff_matrix: The square payoff matrix. game: Game that should contain a set of payoff matrices group_size: Size of the group. By default, we assume that interactions are pairwise (the group size is 2). figsize: Size of the figure. This parameter is only used if the ax parameter is not defined. ax: A matplotlib figure axis. Returns ------- A tuple with the simplex object which can be used to add more features to the plot, the function that can be used to calculate gradients and should be passed to `Simplex2D.draw_trajectory_from_roots` and `Simplex2D.draw_scatter_shadow`, a list of the roots in barycentric coordinates, a list of the roots in cartesian coordinates and a list of booleans indicating whether the roots are stable. It also returns the game class (this is important, since a new game is created when passing a payoff matrix, and if not returned, a reference to the game instance will disappear, and it will produce a segmentation fault). Finally, it also returns a reference to the evolver object. """ if (payoff_matrix is None) and (game is None): raise Exception("You need to define either a payoff matrix or a game.") elif game is None: if (group_size is None) or (group_size < 2): raise Exception("group_size not be None and must be >= 2") if group_size == 2: game = Matrix2PlayerGameHolder(payoff_matrix.shape[0], payoff_matrix) else: game = MatrixNPlayerGameHolder(payoff_matrix.shape[0], group_size, payoff_matrix) simplex = Simplex2D(discrete=True, size=population_size, nb_points=population_size + 1) simplex.add_axis(figsize, ax) v = np.asarray(xy_to_barycentric_coordinates(simplex.X, simplex.Y, simplex.corners)) v_int = np.floor(v * population_size).astype(np.int64) evolver = PairwiseComparison(population_size=population_size, game=game) result = np.zeros(shape=(v_int.shape[1], v_int.shape[2], 3)) for i in range(v_int.shape[1]): for j in range(v_int.shape[2]): if v_int[:, i, j].sum() <= population_size: result[i, j, :] = evolver.calculate_gradient_of_selection(beta, v_int[:, i, j]) result = result.swapaxes(0, 1).swapaxes(0, 2) xy_results = vectorized_barycentric_to_xy_coordinates(result, simplex.corners) ux = xy_results[:, :, 0].astype(np.float64) uy = xy_results[:, :, 1].astype(np.float64) simplex.apply_simplex_boundaries_to_gradients(ux, uy) return simplex, lambda u, t: population_size * evolver.calculate_gradient_of_selection(beta, u), game, evolver