11"""Implementation of the Moran process on Graphs."""
22
3- from collections import Counter
43import random
4+ from collections import Counter
5+ from typing import Callable , List , Optional , Set , Tuple
56
67import matplotlib .pyplot as plt
78import numpy as np
89
9- from axelrod import DEFAULT_TURNS , Player , Game
10+ from axelrod import DEFAULT_TURNS , Game , Player
11+
1012from .deterministic_cache import DeterministicCache
11- from .graph import complete_graph , Graph
13+ from .graph import Graph , complete_graph
1214from .match import Match
1315from .random_ import randrange
1416
15- from typing import List , Tuple , Set , Optional
1617
17-
18- def fitness_proportionate_selection (scores : List ) -> int :
18+ def fitness_proportionate_selection (
19+ scores : List , fitness_transformation : Callable = None
20+ ) -> int :
1921 """Randomly selects an individual proportionally to score.
2022
2123 Parameters
2224 ----------
2325 scores: Any sequence of real numbers
26+ fitness_transformation: A function mapping a score to a (non-negative) float
2427
2528 Returns
2629 -------
2730 An index of the above list selected at random proportionally to the list
2831 element divided by the total.
2932 """
30- csums = np .cumsum (scores )
33+ if fitness_transformation is None :
34+ csums = np .cumsum (scores )
35+ else :
36+ csums = np .cumsum ([fitness_transformation (s ) for s in scores ])
3137 total = csums [- 1 ]
3238 r = random .random () * total
3339
@@ -38,13 +44,20 @@ def fitness_proportionate_selection(scores: List) -> int:
3844
3945
4046class MoranProcess (object ):
41- def __init__ (self , players : List [Player ], turns : int = DEFAULT_TURNS ,
42- prob_end : float = None , noise : float = 0 ,
43- game : Game = None ,
44- deterministic_cache : DeterministicCache = None ,
45- mutation_rate : float = 0. , mode : str = 'bd' ,
46- interaction_graph : Graph = None ,
47- reproduction_graph : Graph = None ) -> None :
47+ def __init__ (
48+ self ,
49+ players : List [Player ],
50+ turns : int = DEFAULT_TURNS ,
51+ prob_end : float = None ,
52+ noise : float = 0 ,
53+ game : Game = None ,
54+ deterministic_cache : DeterministicCache = None ,
55+ mutation_rate : float = 0. ,
56+ mode : str = "bd" ,
57+ interaction_graph : Graph = None ,
58+ reproduction_graph : Graph = None ,
59+ fitness_transformation : Callable = None ,
60+ ) -> None :
4861 """
4962 An agent based Moran process class. In each round, each player plays a
5063 Match with each other player. Players are assigned a fitness score by
@@ -92,6 +105,8 @@ def __init__(self, players: List[Player], turns: int = DEFAULT_TURNS,
92105 reproduction_graph: Axelrod.graph.Graph
93106 The reproduction graph, set equal to the interaction graph if not
94107 given
108+ fitness_transformation:
109+ A function mapping a score to a (non-negative) float
95110 """
96111 self .turns = turns
97112 self .prob_end = prob_end
@@ -107,7 +122,7 @@ def __init__(self, players: List[Player], turns: int = DEFAULT_TURNS,
107122 assert (mutation_rate >= 0 ) and (mutation_rate <= 1 )
108123 assert (noise >= 0 ) and (noise <= 1 )
109124 mode = mode .lower ()
110- assert mode in ['bd' , 'db' ]
125+ assert mode in ["bd" , "db" ]
111126 self .mode = mode
112127 if deterministic_cache is not None :
113128 self .deterministic_cache = deterministic_cache
@@ -129,19 +144,22 @@ def __init__(self, players: List[Player], turns: int = DEFAULT_TURNS,
129144 if interaction_graph is None :
130145 interaction_graph = complete_graph (len (players ), loops = False )
131146 if reproduction_graph is None :
132- reproduction_graph = Graph (interaction_graph .edges (),
133- directed = interaction_graph .directed )
147+ reproduction_graph = Graph (
148+ interaction_graph .edges (), directed = interaction_graph .directed
149+ )
134150 reproduction_graph .add_loops ()
135151 # Check equal vertices
136152 v1 = interaction_graph .vertices ()
137153 v2 = reproduction_graph .vertices ()
138154 assert list (v1 ) == list (v2 )
139155 self .interaction_graph = interaction_graph
140156 self .reproduction_graph = reproduction_graph
157+ self .fitness_transformation = fitness_transformation
141158 # Map players to graph vertices
142159 self .locations = sorted (interaction_graph .vertices ())
143- self .index = dict (zip (sorted (interaction_graph .vertices ()),
144- range (len (players ))))
160+ self .index = dict (
161+ zip (sorted (interaction_graph .vertices ()), range (len (players )))
162+ )
145163
146164 def set_players (self ) -> None :
147165 """Copy the initial players into the first population."""
@@ -192,8 +210,9 @@ def death(self, index: int = None) -> int:
192210 else :
193211 # Select locally
194212 # index is not None in this case
195- vertex = random .choice (sorted (
196- self .reproduction_graph .out_vertices (self .locations [index ])))
213+ vertex = random .choice (
214+ sorted (self .reproduction_graph .out_vertices (self .locations [index ]))
215+ )
197216 i = self .index [vertex ]
198217 return i
199218
@@ -212,11 +231,15 @@ def birth(self, index: int = None) -> int:
212231 # possible choices
213232 scores .pop (index )
214233 # Make sure to get the correct index post-pop
215- j = fitness_proportionate_selection (scores )
234+ j = fitness_proportionate_selection (
235+ scores , fitness_transformation = self .fitness_transformation
236+ )
216237 if j >= index :
217238 j += 1
218239 else :
219- j = fitness_proportionate_selection (scores )
240+ j = fitness_proportionate_selection (
241+ scores , fitness_transformation = self .fitness_transformation
242+ )
220243 return j
221244
222245 def fixation_check (self ) -> bool :
@@ -321,11 +344,14 @@ def score_all(self) -> List:
321344 for i , j in self ._matchup_indices ():
322345 player1 = self .players [i ]
323346 player2 = self .players [j ]
324- match = Match ((player1 , player2 ),
325- turns = self .turns , prob_end = self .prob_end ,
326- noise = self .noise ,
327- game = self .game ,
328- deterministic_cache = self .deterministic_cache )
347+ match = Match (
348+ (player1 , player2 ),
349+ turns = self .turns ,
350+ prob_end = self .prob_end ,
351+ noise = self .noise ,
352+ game = self .game ,
353+ deterministic_cache = self .deterministic_cache ,
354+ )
329355 match .play ()
330356 match_scores = match .final_score_per_turn ()
331357 scores [i ] += match_scores [0 ]
@@ -373,7 +399,8 @@ def play(self) -> List[Counter]:
373399 if self .mutation_rate != 0 :
374400 raise ValueError (
375401 "MoranProcess.play() will never exit if mutation_rate is"
376- "nonzero. Use iteration instead." )
402+ "nonzero. Use iteration instead."
403+ )
377404 while True :
378405 try :
379406 self .__next__ ()
@@ -435,8 +462,10 @@ class ApproximateMoranProcess(MoranProcess):
435462 Instead of playing the matches, the result is sampled
436463 from a dictionary of player tuples to distribution of match outcomes
437464 """
438- def __init__ (self , players : List [Player ], cached_outcomes : dict ,
439- mutation_rate : float = 0 ) -> None :
465+
466+ def __init__ (
467+ self , players : List [Player ], cached_outcomes : dict , mutation_rate : float = 0
468+ ) -> None :
440469 """
441470 Parameters
442471 ----------
@@ -448,8 +477,12 @@ def __init__(self, players: List[Player], cached_outcomes: dict,
448477 probability `mutation_rate`
449478 """
450479 super (ApproximateMoranProcess , self ).__init__ (
451- players , turns = 0 , noise = 0 , deterministic_cache = None ,
452- mutation_rate = mutation_rate )
480+ players ,
481+ turns = 0 ,
482+ noise = 0 ,
483+ deterministic_cache = None ,
484+ mutation_rate = mutation_rate ,
485+ )
453486 self .cached_outcomes = cached_outcomes
454487
455488 def score_all (self ) -> List :
@@ -466,8 +499,7 @@ def score_all(self) -> List:
466499 scores = [0 ] * N
467500 for i in range (N ):
468501 for j in range (i + 1 , N ):
469- player_names = tuple ([str (self .players [i ]),
470- str (self .players [j ])])
502+ player_names = tuple ([str (self .players [i ]), str (self .players [j ])])
471503
472504 cached_score = self ._get_scores_from_cache (player_names )
473505 scores [i ] += cached_score [0 ]
0 commit comments