Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions examples/axelrod/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@


def main():
"""Example of usage of Population with axelrod"""
# TODO: @Scezaquer document this example (i.e., what the purpose?)
"""Example: managing an evolving population with axelrod.

We create a population of agents, and then
1- Make the agents play a tournament
2- Select the worst and best performing individuals
3- Replace the worst agent with a copy of the best

We repeat these steps for a few generations. This reproduces the original
axelrod paper's concept.
"""

# Create a population
players = [
axl.Cooperator(), axl.Defector(), axl.TitForTat(),
axl.Grudger(), axl.Alternator(), axl.Aggravater(),
Expand All @@ -13,27 +23,35 @@ def main():
]

with Population() as pop:
# Each player has it's own branch (lineage) in the population
branches = [
pop.branch(str(p)) for p in players]

# Commit the initial population of agents to their respective branches
for player, branch in zip(players, branches):
pop.checkout(branch)
pop.commit(player)

# Make a few generations of a tournament
for i in range(7):
# Play the tournament
tournament = axl.Tournament(players)
results = tournament.play()

# Pick the best and worst players
first = results.ranking[0]
last = results.ranking[-1]

# Replace the worst player with a copy of the best
pop.checkout(branches[first])

branches[last] = pop.branch(
str(players[first]) + str(i)
)
players[last] = players[first]

# Commit the members of the new generation to their respective
# branches
for player, branch in zip(players, branches):
pop.checkout(branch)
pop.commit(player)
Expand Down
29 changes: 18 additions & 11 deletions examples/dna/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,42 @@
from popcore.population import Population
import random

# TODO: @Szacquer document this example.

def random_linear_dna_evolution():
"""Tracking the evolution of a strand of DNA

def mutate(parent_parameters, hyperparameters, contributors=[]):
"""Mutate a strand of DNA (replace a character in the str at random)"""
next_dna = list(parent_parameters)
next_dna[hyperparameters["spot"]] = hyperparameters["letter"]
next_dna = ''.join(next_dna)
return next_dna, hyperparameters
We look at a single strand of DNA, represented by a string, as it mutates
over time. We use a Population to store it's evolution.

Here, we only consider a single lineage and suppose that no speciation
happened."""

def mutate(parent_parameters, hyperparameters):
"""Mutate a strand of DNA (replace a character in the str at random)"""
next_dna = list(parent_parameters)
next_dna[hyperparameters["spot"]] = hyperparameters["letter"]
next_dna = ''.join(next_dna)
return next_dna, hyperparameters

def random_linear_dna_evolution():
"""This tests the correctness of the case where the population consists
of only a single lineage"""
pop = Population()

next_dna = "OOOOO"
# Initial DNA strand
next_dna = "AAAAA"
dna_history = [next_dna]

# Commit the first DNA strand
pop.commit(parameters=next_dna)

for x in range(16):
# Mutate the DNA strand
letter = random.choice("ACGT")
spot = random.randrange(len(next_dna))

hyperparameters = {"letter": letter, "spot": spot}
next_dna, _ = mutate(next_dna, hyperparameters)
dna_history.append(next_dna)

# Commit the new, mutated DNA strand to the population
pop.commit(parameters=next_dna, hyperparameters=hyperparameters)

return pop, dna_history
Expand Down
19 changes: 10 additions & 9 deletions examples/dna/population_dna.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,9 @@ def mutate(parent_parameters, hyperparameters, contributors=[]):
def test_visual_construction(self):
"""Tree tracking the evolution of a strand of DNA along 3 evolutionary
paths"""
# Visual test, uncomment the last line to see what the resulting trees
# look like and check that they make sense.

with Population() as pop:
# tree.add_root("GGTCAACAAATCATAAAGATATTGG") # Land snail DNA
new_DNA = "OOOOO"
new_DNA = "GGTCAACAAATCATAAAGATATTGG" # Land snail DNA
pop.branch("Lineage 1")
pop.branch("Lineage 2")
pop.branch("Lineage 3")
Expand All @@ -51,11 +48,15 @@ def test_visual_construction(self):
pop.checkout(branch)

hyperparameters = {"letter": letter, "spot": spot}
new_DNA, _ = TestPopulation.mutate(pop._player.parameters,
hyperparameters)

pop.commit(parameters=new_DNA,
hyperparameters=hyperparameters)
new_DNA, _ = TestPopulation.mutate(
pop._player.parameters,
hyperparameters
)

pop.commit(
parameters=new_DNA,
hyperparameters=hyperparameters
)

def test_linear(self):
"""This tests the correctness of the case where the population consists
Expand Down
1 change: 1 addition & 0 deletions src/popcore/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .core import ( # noqa
Player, Interaction, Team
)
from .population import Population
147 changes: 64 additions & 83 deletions src/popcore/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,22 @@

class Player:
"""
Player
A specific version of an agent at a given point in time.

This is equivalent to a commit in the population.

:param str id: The id of the player to find it in the population.
ids must be unique within each population. Defaults to None.
:param Optional[Player] parent: The parent of this player.
If None, this is considered a root. Every player may only
have one parent. Defaults to None
:param Optional[Interaction] interaction: __description__
:param Optional[int] generation: The generation this player belongs to.
Defaults to 0.
:param Optional[int] timestep: The timestep when this player was
created. Defaults to 1.
"""

def __init__(
self,
id: Optional[str] = None,
Expand All @@ -17,47 +31,7 @@ def __init__(
timestep: Optional[int] = 1,
branch: Optional[str] = None,
):

"""A specific version of an agent at a given point in time.

This is equivalent to a commit in the population.

Args: TODO
parent (Player | None): The parent of this player.
If None, this is considered a root. Every player may only
have one parent, but if it needs more, it can have
arbitrarily many contributors. Defaults to None
model_parameters (Any): The parameters of the model. With
neural networks, that would be the weights and biases.
Defaults to None.
id_str (str): The id_str of the player to find it in the pop.
id_strs must be unique within each pop. Defaults to the empty
string.
hyperparameters (Dict[str, Any]): A dictionary of the
hyperparameters that define the transition from the parent
to this player. This should contain enough information to
reproduce the evolution step deterministically given the
parent and contributors parameters.
Defaults to an empty dict.
contributors (List[PhylogeneticTree.Node]): All the models
other than the parent that contributed to the evolution.
Typically, that would be opponents and allies, or mates in
the case of genetic crossover.
For example, if the model played a game of chess against
an opponent and learned from it, the parent would be the
model before that game, and the contributor would be the
opponent. Defaults to an empty list.
generation (int): The generation this player belongs to.
Defaults to 1.
timestep (int): The timestep when this player was created.
Defaults to 1.

Raises:
KeyError: If hyperparameters does not contain one of the
variables that were defined as necessary when creating the
tree.
ValueError: If the id_str conflicts with an other node in the tree.
"""
# TODO: Should this raise an error if the id is already taken?
self.id = id
self.parent = parent
self.descendants: List[Player] = []
Expand All @@ -77,38 +51,33 @@ def add_descendant(
branch: Optional[str] = None
) -> 'Player':

"""Adds a decendant to this node

If `node` is directly specified then it will be added as a child.
Otherwise, the other parameters will be used to create a new node
and add it as a child.

Args:
model_parameters (Any): The model parameters of the child to be
added. Defaults to None.
id_str (str): The id_str of the child. If this is the empty string,
a unique id_str will be picked at random.
Defaults to the empty string.
hyperparameters (Dict[str, Any]): A dictionary of the
hyperparameters that define the transition from this node
to the new child. This should contain enough information to
reproduce the evolution step deterministically given the
parent and contributors parameters.
Defaults to an empty dict.
interaction (List[Player]): All the models
other than the parent that contributed to the evolution.
Typically, that would be opponents and allies, or mates in
the case of genetic crossover.
For example, if the model played a game of chess against
an opponent and learned from it, the parent would be the
model before that game, and the contributor would be the
opponent. Defaults to an empty list.

Returns:
Player: The new descendant

"""Adds a decendant to this player.

:param Optional[str] id: The id of the child.
Defaults to None.
:param Optional[Interaction] interaction: All the models
other than the parent that contributed to the evolution.
Typically, that would be opponents and allies, or mates in
the case of genetic crossover.
For example, if the model played a game of chess against
an opponent and learned from it, the parent would be the
model before that game, and the contributor would be the
opponent. Defaults to None.
TODO: update interaction description.
:param Optional[int] timestep: The timestep when the descendent was
created. Defaults to 1.
:param Optional[str] branch: The branch this descendent belongs to.
Defaults to None.

:return: The new descendant
:rtype: Player

.. seealso::
:meth:`popcore.Player.has_descendants`
"""

# TODO: Should this check the branch exists or create it otherwise?

branch = self.branch if branch is None else branch

# Create child node
Expand All @@ -126,12 +95,26 @@ def add_descendant(
return descendant

def has_descendants(self) -> bool:
"""Returns True if the player has descendants

.. seealso::
:meth:`popcore.Player.add_descendant`"""
return len(self.descendants) > 0


class Team(Player):
"""
Team
A Team is a Player with an additional `members` attribute which is a
list of (sub)players that make up the team.

:param str id: The id of the Team. ids must be unique within each
population.
:param list[Player] members: The players that constitute the team.

.. seealso::
:class:`popcore.Player`

:class:`popcore.Population`
"""
members: "list[Player]"

Expand All @@ -141,9 +124,14 @@ def __init__(self, id: str, members: "list[Player]"):


class Interaction(Generic[GameOutcome]):
"""_summary_
players: players involved in the game
scores: outcomes for each player involved in the game
"""A list of the players that took part in an interaction, and their
individual outcomes.

:param List[Player] players: Players involved in the game.
:param Lists[GameOutcome] outcomes: outcomes for each player involved in
the game.
:param int timestep: The timestep when the interaction occured. Defaults to
0.
"""

def __init__(
Expand All @@ -152,13 +140,6 @@ def __init__(
outcomes: List[GameOutcome],
timestep: int = 0
):
"""_summary_

Args:
players (List[Player]): TODO _description_
outcomes (List[OUTCOME]): TODO _description_
timestep (int): TODO: description
"""
assert len(players) == len(outcomes)
assert timestep >= 0
self._players = players
Expand Down
Loading