@@ -31,7 +31,33 @@ def fitness_proportionate_selection(scores):
3131
3232
3333class MoranProcess (object ):
34- def __init__ (self , players , turns = 100 , noise = 0 , deterministic_cache = None ):
34+ def __init__ (self , players , turns = 100 , noise = 0 , deterministic_cache = None , mutation_rate = 0. ):
35+ """
36+ An agent based Moran process class. In each round, each player plays a Match with each other
37+ player. Players are assigned a fitness score by their total score from all matches in the round.
38+ A player is chosen to reproduce proportionally to fitness, possibly mutated, and is cloned. The
39+ clone replaces a randomly chosen player.
40+
41+ If the mutation_rate is 0, the population will eventually fixate on exactly one player type. In this
42+ case a StopIteration exception is raised and the play stops. If mutation_rate is not zero, then
43+ the process will iterate indefinitely, so mp.play() will never exit, and you should use the class as an
44+ iterator instead.
45+
46+ When a player mutates it chooses a random player type from the initial population. This is not the only
47+ method yet emulates the common method in the literature.
48+
49+ Parameters
50+ ----------
51+ players, iterable of axelrod.Player subclasses
52+ turns: int, 100
53+ The number of turns in each pairwise interaction
54+ noise: float, 0
55+ The background noise, if any. Randomly flips plays with probability `noise`.
56+ deterministic_cache: axelrod.DeterministicCache, None
57+ A optional prebuilt deterministic cache
58+ mutation_rate: float, 0
59+ The rate of mutation. Replicating players are mutated with probability `mutation_rate`
60+ """
3561 self .turns = turns
3662 self .noise = noise
3763 self .initial_players = players # save initial population
@@ -40,10 +66,24 @@ def __init__(self, players, turns=100, noise=0, deterministic_cache=None):
4066 self .set_players ()
4167 self .score_history = []
4268 self .winning_strategy_name = None
69+ self .mutation_rate = mutation_rate
70+ assert (mutation_rate >= 0 ) and (mutation_rate <= 1 )
71+ assert (noise >= 0 ) and (noise <= 1 )
4372 if deterministic_cache is not None :
4473 self .deterministic_cache = deterministic_cache
4574 else :
4675 self .deterministic_cache = DeterministicCache ()
76+ # Build the set of mutation targets
77+ # Determine the number of unique types (players)
78+ keys = set ([str (p ) for p in players ])
79+ # Create a dictionary mapping each type to a set of representatives of the other types
80+ d = dict ()
81+ for p in players :
82+ d [str (p )] = p
83+ mutation_targets = dict ()
84+ for key in sorted (keys ):
85+ mutation_targets [key ] = [v for (k , v ) in sorted (d .items ()) if k != key ]
86+ self .mutation_targets = mutation_targets
4787
4888 def set_players (self ):
4989 """Copy the initial players into the first population."""
@@ -60,29 +100,49 @@ def _stochastic(self):
60100 A boolean to show whether a match between two players would be
61101 stochastic
62102 """
63- return is_stochastic (self .players , self .noise )
103+ return is_stochastic (self .players , self .noise ) or (self .mutation_rate > 0 )
104+
105+ def mutate (self , index ):
106+ # If mutate, choose another strategy at random from the initial population
107+ r = random .random ()
108+ if r < self .mutation_rate :
109+ s = str (self .players [index ])
110+ j = randrange (0 , len (self .mutation_targets [s ]))
111+ p = self .mutation_targets [s ][j ]
112+ new_player = p .clone ()
113+ else :
114+ # Just clone the player
115+ new_player = self .players [index ].clone ()
116+ return new_player
64117
65118 def __next__ (self ):
66119 """Iterate the population:
67120 - play the round's matches
68121 - chooses a player proportionally to fitness (total score) to reproduce
122+ - mutate, if appropriate
69123 - choose a player at random to be replaced
70124 - update the population
71125 """
72126 # Check the exit condition, that all players are of the same type.
73- classes = set (p . __class__ for p in self .players )
74- if len (classes ) == 1 :
127+ classes = set (str ( p ) for p in self .players )
128+ if ( self . mutation_rate == 0 ) and ( len (classes ) == 1 ) :
75129 self .winning_strategy_name = str (self .players [0 ])
76130 raise StopIteration
77131 scores = self ._play_next_round ()
78132 # Update the population
79133 # Fitness proportionate selection
80134 j = fitness_proportionate_selection (scores )
135+ # Mutate?
136+ if self .mutation_rate :
137+ new_player = self .mutate (j )
138+ else :
139+ new_player = self .players [j ].clone ()
81140 # Randomly remove a strategy
82141 i = randrange (0 , len (self .players ))
83142 # Replace player i with clone of player j
84- self .players [i ] = self . players [ j ]. clone ()
143+ self .players [i ] = new_player
85144 self .populations .append (self .population_distribution ())
145+ return self
86146
87147 def _play_next_round (self ):
88148 """Plays the next round of the process. Every player is paired up
@@ -123,6 +183,8 @@ def reset(self):
123183
124184 def play (self ):
125185 """Play the process out to completion."""
186+ if self .mutation_rate != 0 :
187+ raise ValueError ("MoranProcess.play() will never exit if mutation_rate is nonzero" )
126188 while True :
127189 try :
128190 self .__next__ ()
0 commit comments