1
1
"""Implementation of the Moran process on Graphs."""
2
2
3
- from collections import Counter
4
3
import random
4
+ from collections import Counter
5
+ from typing import Callable , List , Optional , Set , Tuple
5
6
6
7
import matplotlib .pyplot as plt
7
8
import numpy as np
8
9
9
- from axelrod import DEFAULT_TURNS , Player , Game
10
+ from axelrod import DEFAULT_TURNS , Game , Player
11
+
10
12
from .deterministic_cache import DeterministicCache
11
- from .graph import complete_graph , Graph
13
+ from .graph import Graph , complete_graph
12
14
from .match import Match
13
15
from .random_ import randrange
14
16
15
- from typing import List , Tuple , Set , Optional
16
17
17
-
18
- def fitness_proportionate_selection (scores : List ) -> int :
18
+ def fitness_proportionate_selection (
19
+ scores : List , fitness_transformation : Callable = None
20
+ ) -> int :
19
21
"""Randomly selects an individual proportionally to score.
20
22
21
23
Parameters
22
24
----------
23
25
scores: Any sequence of real numbers
26
+ fitness_transformation: A function mapping a score to a (non-negative) float
24
27
25
28
Returns
26
29
-------
27
30
An index of the above list selected at random proportionally to the list
28
31
element divided by the total.
29
32
"""
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 ])
31
37
total = csums [- 1 ]
32
38
r = random .random () * total
33
39
@@ -38,13 +44,20 @@ def fitness_proportionate_selection(scores: List) -> int:
38
44
39
45
40
46
class 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 :
48
61
"""
49
62
An agent based Moran process class. In each round, each player plays a
50
63
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,
92
105
reproduction_graph: Axelrod.graph.Graph
93
106
The reproduction graph, set equal to the interaction graph if not
94
107
given
108
+ fitness_transformation:
109
+ A function mapping a score to a (non-negative) float
95
110
"""
96
111
self .turns = turns
97
112
self .prob_end = prob_end
@@ -107,7 +122,7 @@ def __init__(self, players: List[Player], turns: int = DEFAULT_TURNS,
107
122
assert (mutation_rate >= 0 ) and (mutation_rate <= 1 )
108
123
assert (noise >= 0 ) and (noise <= 1 )
109
124
mode = mode .lower ()
110
- assert mode in ['bd' , 'db' ]
125
+ assert mode in ["bd" , "db" ]
111
126
self .mode = mode
112
127
if deterministic_cache is not None :
113
128
self .deterministic_cache = deterministic_cache
@@ -129,19 +144,22 @@ def __init__(self, players: List[Player], turns: int = DEFAULT_TURNS,
129
144
if interaction_graph is None :
130
145
interaction_graph = complete_graph (len (players ), loops = False )
131
146
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
+ )
134
150
reproduction_graph .add_loops ()
135
151
# Check equal vertices
136
152
v1 = interaction_graph .vertices ()
137
153
v2 = reproduction_graph .vertices ()
138
154
assert list (v1 ) == list (v2 )
139
155
self .interaction_graph = interaction_graph
140
156
self .reproduction_graph = reproduction_graph
157
+ self .fitness_transformation = fitness_transformation
141
158
# Map players to graph vertices
142
159
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
+ )
145
163
146
164
def set_players (self ) -> None :
147
165
"""Copy the initial players into the first population."""
@@ -192,8 +210,9 @@ def death(self, index: int = None) -> int:
192
210
else :
193
211
# Select locally
194
212
# 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
+ )
197
216
i = self .index [vertex ]
198
217
return i
199
218
@@ -212,11 +231,15 @@ def birth(self, index: int = None) -> int:
212
231
# possible choices
213
232
scores .pop (index )
214
233
# 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
+ )
216
237
if j >= index :
217
238
j += 1
218
239
else :
219
- j = fitness_proportionate_selection (scores )
240
+ j = fitness_proportionate_selection (
241
+ scores , fitness_transformation = self .fitness_transformation
242
+ )
220
243
return j
221
244
222
245
def fixation_check (self ) -> bool :
@@ -321,11 +344,14 @@ def score_all(self) -> List:
321
344
for i , j in self ._matchup_indices ():
322
345
player1 = self .players [i ]
323
346
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
+ )
329
355
match .play ()
330
356
match_scores = match .final_score_per_turn ()
331
357
scores [i ] += match_scores [0 ]
@@ -373,7 +399,8 @@ def play(self) -> List[Counter]:
373
399
if self .mutation_rate != 0 :
374
400
raise ValueError (
375
401
"MoranProcess.play() will never exit if mutation_rate is"
376
- "nonzero. Use iteration instead." )
402
+ "nonzero. Use iteration instead."
403
+ )
377
404
while True :
378
405
try :
379
406
self .__next__ ()
@@ -435,8 +462,10 @@ class ApproximateMoranProcess(MoranProcess):
435
462
Instead of playing the matches, the result is sampled
436
463
from a dictionary of player tuples to distribution of match outcomes
437
464
"""
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 :
440
469
"""
441
470
Parameters
442
471
----------
@@ -448,8 +477,12 @@ def __init__(self, players: List[Player], cached_outcomes: dict,
448
477
probability `mutation_rate`
449
478
"""
450
479
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
+ )
453
486
self .cached_outcomes = cached_outcomes
454
487
455
488
def score_all (self ) -> List :
@@ -466,8 +499,7 @@ def score_all(self) -> List:
466
499
scores = [0 ] * N
467
500
for i in range (N ):
468
501
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 ])])
471
503
472
504
cached_score = self ._get_scores_from_cache (player_names )
473
505
scores [i ] += cached_score [0 ]
0 commit comments