Skip to content

Commit f375af3

Browse files
Nikoleta-v3drvinceknight
authored andcommitted
Pass fitness function moran process (#1198)
* write test for fitness function moran process * implement fitness function for the moran process * rename test * add documentation * run black and isort * add fitness_function to the docstrings * fix type hint error * rename fitness_function to fitness_transformation
1 parent 695a3e3 commit f375af3

File tree

4 files changed

+172
-87
lines changed

4 files changed

+172
-87
lines changed

axelrod/moran.py

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
11
"""Implementation of the Moran process on Graphs."""
22

3-
from collections import Counter
43
import random
4+
from collections import Counter
5+
from typing import Callable, List, Optional, Set, Tuple
56

67
import matplotlib.pyplot as plt
78
import numpy as np
89

9-
from axelrod import DEFAULT_TURNS, Player, Game
10+
from axelrod import DEFAULT_TURNS, Game, Player
11+
1012
from .deterministic_cache import DeterministicCache
11-
from .graph import complete_graph, Graph
13+
from .graph import Graph, complete_graph
1214
from .match import Match
1315
from .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

4046
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:
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

Comments
 (0)