├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── test_version.py │ └── test_utils.py ├── algorithms │ ├── __init__.py │ └── test_particle_swarm_optimization.py └── integration │ ├── __init__.py │ ├── test_cycler_integration.py │ ├── test_population_sizes.py │ ├── test_gambler.py │ ├── test_fsm.py │ └── test_hmm.py ├── src └── axelrod_dojo │ ├── algorithms │ ├── __init__.py │ ├── particle_swarm_optimization.py │ └── evolutionary_algorithm.py │ ├── version.py │ ├── __init__.py │ ├── arguments.py │ └── utils.py ├── requirements.txt ├── docs ├── howtos │ ├── train-using-genetic-algorithm.rst │ ├── train-using-particle-swarm-algorithm.rst │ ├── index.rst │ └── use-different-ojective-functions.rst ├── reference │ ├── index.rst │ └── bibliography.rst ├── background │ ├── index.rst │ └── genetic_algorithm.rst ├── Makefile ├── index.rst ├── tutorial │ └── index.rst └── conf.py ├── .travis.yml ├── bin ├── analyze_data.py ├── fsm_evolve.py ├── hmm_evolve.py ├── ann_evolve.py ├── lookup_evolve.py └── pso_evolve.py ├── doctests.py ├── tests.py ├── .gitignore ├── setup.py ├── LICENSE.txt ├── lookup_tables.csv └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/algorithms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/axelrod_dojo/algorithms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/axelrod_dojo/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.8' 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | axelrod>=4.3.0 2 | fsspec>=0.4.3 3 | dask>=2.3.0 4 | docopt>=0.6.2 5 | pyswarm>=0.6 6 | -------------------------------------------------------------------------------- /docs/howtos/train-using-genetic-algorithm.rst: -------------------------------------------------------------------------------- 1 | Train using the genetic algorithm 2 | ================================= 3 | 4 | WIP: include all details for training with genetic algorithm. 5 | -------------------------------------------------------------------------------- /docs/howtos/train-using-particle-swarm-algorithm.rst: -------------------------------------------------------------------------------- 1 | Train using the particle swarm algorithm 2 | ======================================== 3 | 4 | WIP: include all details for training with PSO 5 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | This section is the reference guide for the various components of the library. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | bibliography.rst 10 | -------------------------------------------------------------------------------- /tests/unit/test_version.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import axelrod_dojo as dojo 3 | 4 | 5 | class TestVersion(unittest.TestCase): 6 | def test_version_exists(self): 7 | self.assertIsInstance(dojo.__version__, str) 8 | -------------------------------------------------------------------------------- /docs/howtos/index.rst: -------------------------------------------------------------------------------- 1 | How to 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | use-different-ojective-functions.rst 8 | train-using-genetic-algorithm.rst 9 | train-using-particle-swarm-algorithm.rst 10 | -------------------------------------------------------------------------------- /src/axelrod_dojo/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | from .arguments import invoke_training 3 | from .algorithms.evolutionary_algorithm import Population 4 | from .algorithms.particle_swarm_optimization import PSO 5 | from .utils import prepare_objective, load_params, PlayerInfo 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | services: 3 | - xvfb 4 | language: python 5 | python: 6 | - 3.7 7 | - 3.6 8 | install: 9 | - pip install -r requirements.txt 10 | - pip install coverage 11 | - pip install coveralls 12 | script: 13 | - python setup.py develop 14 | - coverage run --source=src -m unittest discover tests -v 15 | - coverage report -m 16 | - python doctests.py 17 | after_success: 18 | - coveralls 19 | -------------------------------------------------------------------------------- /docs/reference/bibliography.rst: -------------------------------------------------------------------------------- 1 | .. _bibliography: 2 | 3 | Bibliography 4 | ============ 5 | 6 | This is a collection of various bibliographic items referenced in the 7 | documentation. 8 | 9 | .. [Harper2017] Marc Harper, Vincent Knight, Martin Jones, Georgios Koutsovoulo, Nikoleta E. Glynatsi and Owen Campbell (2017) Reinforcement Learning Produces Dominant Strategies for the Iterated Prisoner's Dilemma. Arxiv. http://arxiv.org/abs/1707.06307 10 | -------------------------------------------------------------------------------- /bin/analyze_data.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import sys 3 | 4 | 5 | def read_data(filename): 6 | """Read in the n top performing results from a given file""" 7 | results = [] 8 | with open(filename) as data: 9 | reader = csv.reader(data) 10 | for line in reader: 11 | results.append((float(line[-2]), line[-1])) 12 | return results 13 | 14 | 15 | if __name__ == "__main__": 16 | data_filename = sys.argv[1] 17 | results = read_data(data_filename) 18 | results.sort() 19 | for result in results[-10:]: 20 | print(result[0], result[1]) 21 | -------------------------------------------------------------------------------- /doctests.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import os 3 | import unittest 4 | import warnings 5 | 6 | 7 | # Note loader and ignore are required arguments for unittest even if unused. 8 | def load_tests(loader, tests, ignore): 9 | for root, dirs, files in os.walk("."): 10 | for f in files: 11 | if f.endswith(".rst"): 12 | tests.addTests( 13 | doctest.DocFileSuite(os.path.join(root, f), 14 | optionflags=doctest.ELLIPSIS)) 15 | 16 | return tests 17 | 18 | 19 | if __name__ == '__main__': 20 | warnings.simplefilter("ignore") 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /docs/background/index.rst: -------------------------------------------------------------------------------- 1 | Background 2 | ========== 3 | 4 | Note that there are currently two algorithms implemented: 5 | 6 | - Genetic algorithm 7 | - Particle swam optimisation 8 | 9 | Note that these two algorithms are not equally suited to each archetype. For 10 | example the Genetic algorithm is believed to be better suited to discrete space 11 | strategies such as the finite state machines whilst the Particle swarm algorithm 12 | would be better suited to a continuous space strategy such as the Gambler. 13 | 14 | For more information on these algorithms and their implementations see: 15 | 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | genetic_algorithm.rst 21 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | from hmm_evolve import HMMParams 5 | from ann_evolve import ANNParams 6 | from fsm_evolve import FSMParams 7 | from lookup_evolve import LookerUpParams 8 | from evolve_utils import load_params 9 | 10 | 11 | class TestRepr(unittest.TestCase): 12 | 13 | def test_repr_load(self): 14 | for params_class, filename in [ 15 | (ANNParams, "ann_params.csv"), 16 | (LookerUpParams, "lookup_params.csv"), 17 | (FSMParams, "fsm_params.csv"), 18 | (HMMParams, "hmm_params.csv") 19 | ]: 20 | print(filename) 21 | params = load_params(params_class, filename, 10) 22 | 23 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = Axelrod-dojo 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/howtos/use-different-ojective-functions.rst: -------------------------------------------------------------------------------- 1 | Use different objective functions 2 | ================================= 3 | 4 | It is currently possible to optimise players for 3 different objectives: 5 | 6 | - Score; 7 | - Score difference; 8 | - Probability of fixation in a Moran process. 9 | 10 | This is done by passing a different objective :code:`name` to the 11 | :code:`prepare_objective` function:: 12 | 13 | >>> import axelrod_dojo as dojo 14 | >>> score_objective = dojo.prepare_objective(name="score", turns=10, repetitions=1) 15 | >>> diff_objective = dojo.prepare_objective(name="score_diff", turns=10, repetitions=1) 16 | >>> moran_objective = dojo.prepare_objective(name="moran", turns=10, repetitions=1) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | data/ 59 | 60 | *.mypy* 61 | 62 | # pycharm 63 | .idea/ 64 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Axelrod-dojo documentation master file, created by 2 | sphinx-quickstart on Wed Oct 11 09:40:19 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Axelrod-dojo's documentation! 7 | ======================================== 8 | 9 | This library is a companion library to the `Axelrod 10 | `_ library: a research tool for the 11 | study of the iterated prisoners dilemma. The **Axelrod-dojo** is used to train 12 | strategies. 13 | 14 | This is done using implementations of: 15 | 16 | - Strategy archetypes :code:`Parameters` 17 | - Algorithms 18 | 19 | Table of Contents 20 | ----------------- 21 | 22 | .. toctree:: 23 | :maxdepth: 3 24 | 25 | tutorial/index.rst 26 | howtos/index.rst 27 | background/index.rst 28 | reference/index.rst 29 | 30 | 31 | Indices and tables 32 | ================== 33 | 34 | * :ref:`genindex` 35 | * :ref:`modindex` 36 | * :ref:`search` 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import unittest 3 | import doctest 4 | import os 5 | 6 | # Read in the version number 7 | exec(open('src/axelrod_dojo/version.py', 'r').read()) 8 | 9 | # Read in the requirements.txt file 10 | with open('requirements.txt') as f: 11 | requirements = [] 12 | for library in f.read().splitlines(): 13 | if "docopt" not in library: # Skip: used only for command line scripts 14 | requirements.append(library) 15 | 16 | setup( 17 | name='axelrod_dojo', 18 | version=__version__, 19 | install_requires=requirements, 20 | author='Marc Harper; Vince Knight; Martin Jones; Georgios Koutsovoulos', 21 | packages=find_packages('src'), 22 | package_dir={"": "src"}, 23 | url='', 24 | license='The MIT License (MIT)', 25 | description='A library to train strategies for the Iterated Prisoners Dilemma', 26 | classifiers=[ 27 | 'Programming Language :: Python :: 3.6', 28 | 'Programming Language :: Python :: 3.7', 29 | 'Programming Language :: Python :: 3 :: Only', 30 | ], 31 | python_requires='>=3.0', 32 | ) 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Martin Jones, Georgios Koutsovoulos, Marc Harper, 4 | Vincent Knight 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /tests/integration/test_cycler_integration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import unittest 4 | 5 | import axelrod as axl 6 | import axelrod_dojo as axl_dojo 7 | 8 | 9 | class TestCyclerParams(unittest.TestCase): 10 | def setUp(self): 11 | pass 12 | 13 | def test_default_single_opponent_e2e(self): 14 | temp_file = tempfile.NamedTemporaryFile() 15 | # we will set the objective to be 16 | cycler_objective = axl_dojo.prepare_objective(name="score", turns=10, repetitions=1) 17 | 18 | # Lets use an opponent_list of just one: 19 | opponent_list = [axl.TitForTat(), axl.Calculator()] 20 | cycler = axl.EvolvableCycler 21 | 22 | # params to pass through 23 | cycler_kwargs = { 24 | "cycle_length": 10 25 | } 26 | 27 | # assert file is empty to start 28 | self.assertEqual(temp_file.readline(), b'') # note that .readline() reads bytes hence b'' 29 | 30 | population = axl_dojo.Population(player_class=cycler, 31 | params_kwargs=cycler_kwargs, 32 | size=20, 33 | objective=cycler_objective, 34 | output_filename=temp_file.name, 35 | opponents=opponent_list) 36 | 37 | generations = 5 38 | population.run(generations, print_output=False) 39 | 40 | # assert the output file exists and is not empty 41 | self.assertTrue(os.path.exists(temp_file.name)) 42 | self.assertNotEqual(temp_file.readline(), b'') # note that .readline() reads bytes hence b'' 43 | 44 | # close the temp file 45 | temp_file.close() 46 | -------------------------------------------------------------------------------- /bin/fsm_evolve.py: -------------------------------------------------------------------------------- 1 | """ 2 | Finite State Machine Evolver 3 | 4 | Usage: 5 | fsm_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION] 6 | [--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSES] 7 | [--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS] 8 | [--turns TURNS] [--noise NOISE] [--nmoran NMORAN] 9 | [--states NUM_STATES] 10 | 11 | Options: 12 | -h --help Show help 13 | --generations GENERATIONS Generations to run the EA [default: 500] 14 | --population POPULATION Population size [default: 40] 15 | --mu MUTATION_RATE Mutation rate [default: 0.1] 16 | --bottleneck BOTTLENECK Number of individuals to keep from each generation [default: 10] 17 | --processes PROCESSES Number of processes to use [default: 1] 18 | --output OUTPUT_FILE File to write data to [default: fsm_params.csv] 19 | --objective OBJECTIVE Objective function [default: score] 20 | --repetitions REPETITIONS Repetitions in objective [default: 100] 21 | --turns TURNS Turns in each match [default: 200] 22 | --noise NOISE Match noise [default: 0.00] 23 | --nmoran NMORAN Moran Population Size, if Moran objective [default: 4] 24 | --states NUM_STATES Number of FSM states [default: 8] 25 | """ 26 | 27 | from axelrod import EvolvableFSMPlayer 28 | from axelrod_dojo import invoke_training 29 | 30 | 31 | def prepare_player_class_kwargs(arguments): 32 | param_kwargs = { 33 | "num_states": int(arguments['--states']), 34 | "mutation_probability": float(arguments['--mu']), 35 | } 36 | return param_kwargs 37 | 38 | 39 | if __name__ == '__main__': 40 | invoke_training( 41 | __doc__, 42 | 'FSM Evolver 0.4', 43 | EvolvableFSMPlayer, 44 | prepare_player_class_kwargs 45 | ) 46 | -------------------------------------------------------------------------------- /bin/hmm_evolve.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hidden Markov Model Evolver 3 | 4 | Usage: 5 | hmm_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION] 6 | [--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSES] 7 | [--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS] 8 | [--turns TURNS] [--noise NOISE] [--nmoran NMORAN] 9 | [--states NUM_STATES] [--algorithm ALGORITHM] 10 | 11 | Options: 12 | -h --help Show help 13 | --generations GENERATIONS Generations to run the EA [default: 500] 14 | --population POPULATION Population size [default: 40] 15 | --mu MUTATION_RATE Mutation rate [default: 0.1] 16 | --bottleneck BOTTLENECK Number of individuals to keep from each generation [default: 10] 17 | --processes PROCESSES Number of processes to use [default: 1] 18 | --output OUTPUT_FILE File to write data to [default: hmm_params.csv] 19 | --objective OBJECTIVE Objective function [default: score] 20 | --repetitions REPETITIONS Repetitions in objective [default: 100] 21 | --turns TURNS Turns in each match [default: 200] 22 | --noise NOISE Match noise [default: 0.00] 23 | --nmoran NMORAN Moran Population Size, if Moran objective [default: 4] 24 | --states NUM_STATES Number of FSM states [default: 5] 25 | --algorithm ALGORITHM Which algorithm to use (EA for evolutionary algorithm or PS for 26 | particle swarm algorithm) [default: EA] 27 | """ 28 | 29 | 30 | from axelrod import EvolvableHMMPlayer 31 | from axelrod_dojo import invoke_training 32 | 33 | 34 | def prepare_player_class_kwargs(arguments): 35 | param_kwargs = { 36 | "num_states": int(arguments['--states']), 37 | "mutation_probability": float(arguments['--mu']), 38 | } 39 | return param_kwargs 40 | 41 | 42 | if __name__ == '__main__': 43 | invoke_training( 44 | __doc__, 45 | 'HMM Evolver 0.4', 46 | EvolvableHMMPlayer, 47 | prepare_player_class_kwargs 48 | ) 49 | -------------------------------------------------------------------------------- /bin/ann_evolve.py: -------------------------------------------------------------------------------- 1 | """ 2 | ANN evolver. 3 | Trains ANN strategies with an evolutionary algorithm. 4 | 5 | Original version by Martin Jones @mojones: 6 | https://gist.github.com/mojones/b809ba565c93feb8d44becc7b93e37c6 7 | 8 | Usage: 9 | ann_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION] 10 | [--mu mutation_probability] [--bottleneck BOTTLENECK] [--processes PROCESSES] 11 | [--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS] 12 | [--turns TURNS] [--noise NOISE] [--nmoran NMORAN] 13 | [--features FEATURES] [--hidden HIDDEN] [--mu_distance DISTANCE] 14 | 15 | Options: 16 | -h --help Show help 17 | --generations GENERATIONS Generations to run the EA [default: 500] 18 | --population POPULATION Starting population size [default: 40] 19 | --mu mutation_probability Mutation rate [default: 0.1] 20 | --bottleneck BOTTLENECK Number of individuals to keep from each generation [default: 10] 21 | --processes PROCESSES Number of processes to use [default: 1] 22 | --output OUTPUT_FILE File to write data to [default: ann_params.csv] 23 | --objective OBJECTIVE Objective function [default: score] 24 | --repetitions REPETITIONS Repetitions in objective [default: 100] 25 | --turns TURNS Turns in each match [default: 200] 26 | --noise NOISE Match noise [default: 0.00] 27 | --nmoran NMORAN Moran Population Size, if Moran objective [default: 4] 28 | --features FEATURES Number of ANN features [default: 17] 29 | --hidden HIDDEN Number of hidden nodes [default: 10] 30 | --mu_distance DISTANCE Delta max for weights updates [default: 10] 31 | """ 32 | 33 | from axelrod import EvolvableANN 34 | from axelrod_dojo import invoke_training 35 | 36 | 37 | def prepare_player_class_kwargs(arguments): 38 | param_kwargs = { 39 | "num_features": int(arguments['--features']), 40 | "num_hidden": int(arguments['--hidden']), 41 | "mutation_probability": float(arguments['--mu']), 42 | "mutation_distance": float(arguments['--mu_distance']) 43 | } 44 | return param_kwargs 45 | 46 | 47 | if __name__ == '__main__': 48 | invoke_training( 49 | __doc__, 50 | 'ANN Evolver 0.4', 51 | EvolvableANN, 52 | prepare_player_class_kwargs 53 | ) 54 | -------------------------------------------------------------------------------- /bin/lookup_evolve.py: -------------------------------------------------------------------------------- 1 | """Lookup Table Evolver 2 | 3 | Usage: 4 | lookup_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION] 5 | [--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSES] 6 | [--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS] 7 | [--turns TURNS] [--noise NOISE] [--nmoran NMORAN] 8 | [--plays PLAYS] [--op_plays OP_PLAYS] [--op_start_plays OP_START_PLAYS] 9 | 10 | Options: 11 | -h --help Show help 12 | --generations GENERATIONS Generations to run the EA [default: 500] 13 | --population POPULATION Starting population size [default: 40] 14 | --mu MUTATION_RATE Mutation rate [default: 0.1] 15 | --bottleneck BOTTLENECK Number of individuals to keep from each generation [default: 10] 16 | --processes PROCESSES Number of processes to use [default: 1] 17 | --output OUTPUT_FILE File to write data to [default: lookup_params.csv] 18 | --objective OBJECTIVE Objective function [default: score] 19 | --repetitions REPETITIONS Repetitions in objective [default: 100] 20 | --turns TURNS Turns in each match [default: 200] 21 | --noise NOISE Match noise [default: 0.00] 22 | --nmoran NMORAN Moran Population Size, if Moran objective [default: 4] 23 | --plays PLAYS Number of recent plays in the lookup table [default: 2] 24 | --op_plays OP_PLAYS Number of recent plays in the lookup table [default: 2] 25 | --op_start_plays OP_START_PLAYS Number of opponent starting plays in the lookup table [default: 2] 26 | """ 27 | 28 | 29 | from axelrod import Action, EvolvableLookerUp 30 | from axelrod_dojo import invoke_training 31 | 32 | 33 | def prepare_player_class_kwargs(arguments): 34 | plays = int(arguments['--plays']) 35 | op_plays = int(arguments['--op_plays']) 36 | op_start_plays = int(arguments['--op_start_plays']) 37 | table_depth = max(plays, op_plays, op_start_plays) 38 | initial_actions = [Action.C] * table_depth 39 | param_kwargs = { 40 | "parameters": (plays, op_plays, op_start_plays), 41 | "initial_actions": initial_actions, 42 | "mutation_probability": float(arguments['--mu']), 43 | } 44 | return param_kwargs 45 | 46 | 47 | if __name__ == '__main__': 48 | invoke_training( 49 | __doc__, 50 | 'LookerUp Evolver 0.4', 51 | EvolvableLookerUp, 52 | prepare_player_class_kwargs 53 | ) -------------------------------------------------------------------------------- /tests/integration/test_population_sizes.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import unittest 3 | 4 | import axelrod as axl 5 | import axelrod_dojo as axl_dojo 6 | 7 | 8 | class TestPopulationSizes(unittest.TestCase): 9 | 10 | def test_basic_pop_size(self): 11 | # Set up Tmp file 12 | temp_file = tempfile.NamedTemporaryFile() 13 | # we will set the objective to be 14 | cycler_objective = axl_dojo.prepare_objective(name="score", turns=10, repetitions=1) 15 | # Lets use an opponent_list of just one: 16 | opponent_list = [axl.TitForTat()] 17 | # params to pass through 18 | cycler_kwargs = { 19 | "cycle_length": 10 20 | } 21 | 22 | population_size = 20 23 | population = axl_dojo.Population(player_class=axl.EvolvableCycler, 24 | params_kwargs=cycler_kwargs, 25 | size=population_size, 26 | objective=cycler_objective, 27 | output_filename=temp_file.name, 28 | opponents=opponent_list) 29 | 30 | # Before run 31 | self.assertEqual(len(population.population), population_size) 32 | 33 | # After Run 34 | population.run(generations=5, print_output=False) 35 | self.assertEqual(len(population.population), population_size) 36 | 37 | # close the temp file 38 | temp_file.close() 39 | 40 | def test_bottleneck_pop_size(self): 41 | # Set up Tmp file 42 | temp_file = tempfile.NamedTemporaryFile() 43 | # we will set the objective to be 44 | cycler_objective = axl_dojo.prepare_objective(name="score", turns=10, repetitions=1) 45 | # Lets use an opponent_list of just one: 46 | opponent_list = [axl.TitForTat()] 47 | # params to pass through 48 | cycler_kwargs = { 49 | "cycle_length": 10 50 | } 51 | 52 | population_size = 20 53 | population = axl_dojo.Population(player_class=axl.EvolvableCycler, 54 | params_kwargs=cycler_kwargs, 55 | size=population_size, 56 | bottleneck=1, 57 | objective=cycler_objective, 58 | output_filename=temp_file.name, 59 | opponents=opponent_list) 60 | 61 | # Before run 62 | self.assertEqual(len(population.population), population_size) 63 | 64 | # After Run 65 | population.run(generations=5, print_output=False) 66 | self.assertEqual(len(population.population), population_size) 67 | 68 | # close the temp file 69 | temp_file.close() 70 | -------------------------------------------------------------------------------- /bin/pso_evolve.py: -------------------------------------------------------------------------------- 1 | """ 2 | Particle Swarm strategy training code. 3 | 4 | Original version by Georgios Koutsovoulos @GDKO : 5 | https://gist.github.com/GDKO/60c3d0fd423598f3c4e4 6 | Based on Martin Jones @mojones original LookerUp code 7 | 8 | Usage: 9 | pso_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION] 10 | [--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSES] 11 | [--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS] 12 | [--turns TURNS] [--noise NOISE] [--nmoran NMORAN] 13 | [--plays PLAYS] [--op_plays OP_PLAYS] [--op_start_plays OP_START_PLAYS] 14 | [--algorithm ALGORITHM] 15 | 16 | Options: 17 | -h --help Show help 18 | --generations GENERATIONS Generations to run the EA [default: 100] 19 | --population POPULATION Starting population size [default: 40] 20 | --mu MUTATION_RATE Mutation rate [default: 0.1] 21 | --bottleneck BOTTLENECK Number of individuals to keep from each generation [default: 10] 22 | --processes PROCESSES Number of processes to use [default: 1] 23 | --output OUTPUT_FILE File to write data to [default: lookup_params.csv] 24 | --objective OBJECTIVE Objective function [default: score] 25 | --repetitions REPETITIONS Repetitions in objective [default: 100] 26 | --turns TURNS Turns in each match [default: 200] 27 | --noise NOISE Match noise [default: 0.00] 28 | --nmoran NMORAN Moran Population Size, if Moran objective [default: 4] 29 | --plays PLAYS Number of recent plays in the lookup table [default: 2] 30 | --op_plays OP_PLAYS Number of recent plays in the lookup table [default: 2] 31 | --op_start_plays OP_START_PLAYS Number of opponent starting plays in the lookup table [default: 2] 32 | --algorithm ALGORITHM Which algorithm to use (EA for evolutionary algorithm or PS for 33 | particle swarm algorithm) [default: PS] 34 | """ 35 | 36 | 37 | from axelrod import Action, EvolvableGambler 38 | from axelrod_dojo import invoke_training 39 | 40 | 41 | def prepare_player_class_kwargs(arguments): 42 | plays = int(arguments['--plays']) 43 | op_plays = int(arguments['--op_plays']) 44 | op_start_plays = int(arguments['--op_start_plays']) 45 | table_depth = max(plays, op_plays, op_start_plays) 46 | initial_actions = [Action.C] * table_depth 47 | param_kwargs = { 48 | "parameters": (plays, op_plays, op_start_plays), 49 | "initial_actions": initial_actions, 50 | "mutation_probability": float(arguments['--mu']), 51 | } 52 | return param_kwargs 53 | 54 | 55 | if __name__ == '__main__': 56 | invoke_training( 57 | __doc__, 58 | 'PSO Evolver 0.4', 59 | EvolvableGambler, 60 | prepare_player_class_kwargs 61 | ) 62 | 63 | -------------------------------------------------------------------------------- /docs/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ======== 3 | 4 | In this tutorial we will aim to find the best Finite State Machine against a 5 | collection of other strategies from the Axelrod library [Harper2017]_. 6 | 7 | First let us get the collection of opponents against which we aim to train:: 8 | 9 | >>> import axelrod as axl 10 | >>> opponents = [axl.TitForTat(), axl.Alternator(), axl.Defector()] 11 | >>> opponents 12 | [Tit For Tat, Alternator, Defector] 13 | 14 | We are now going to prepare the training algorithm. First of all, we need to 15 | prepare the objective of our strategy. In this case we will aim to maximise 16 | :code:`score` in a match with :code:`10` turns over :code:`1` repetition:: 17 | 18 | >>> import axelrod_dojo as dojo 19 | >>> objective = dojo.prepare_objective(name="score", turns=10, repetitions=1) 20 | 21 | The algorithm we are going to use is a genetic algorithm which requires a 22 | population of individuals. Let us set up the inputs:: 23 | 24 | >>> params_class = axl.EvolvableFSMPlayer 25 | >>> params_kwargs = {"num_states": 2} 26 | 27 | Using this we can now create our Population (with :code:`20` individuals) for a 28 | genetic algorithm:: 29 | 30 | >>> axl.seed(1) 31 | >>> population = dojo.Population(player_class=params_class, 32 | ... params_kwargs=params_kwargs, 33 | ... size=20, 34 | ... objective=objective, 35 | ... output_filename="training_output.csv", 36 | ... opponents=opponents, 37 | ... bottleneck=2, 38 | ... mutation_probability=.1) 39 | 40 | 41 | We can now evolve our population:: 42 | 43 | >>> generations = 4 44 | >>> population.run(generations) 45 | Scoring Generation 1 46 | Generation 1 | Best Score: 2.1333333333333333 47 | Scoring Generation 2 48 | Generation 2 | Best Score: 2.1333333333333333 49 | Scoring Generation 3 50 | Generation 3 | Best Score: 2.1333333333333333 51 | Scoring Generation 4 52 | Generation 4 | Best Score: 2.1333333333333333 53 | 54 | The :code:`run` command prints out the progress of the algorithm and this is 55 | also written to the output file (we passed :code:`output_filename` as an 56 | argument earlier). The printing can be turned off to keep logging to a minimum 57 | by passing :code:`print_output=False` to the :code:`run`. 58 | 59 | The last best score is a finite state machine with representation 60 | :code:`0:C:0_C_0_C:0_D_1_D:1_C_1_D:1_D_1_D` which corresponds to a strategy that 61 | stays in state :code:`0` as long as the opponent cooperates but otherwise goes 62 | to state :code:`1` and defects forever. Indeed, if the strategy is playing 63 | :code:`Defector` or :code:`Alternator` then it should just defect, otherwise it 64 | should cooperate. 65 | -------------------------------------------------------------------------------- /tests/integration/test_gambler.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import tempfile 3 | import csv 4 | 5 | import axelrod as axl 6 | import axelrod_dojo as dojo 7 | 8 | C, D = axl.Action.C, axl.Action.D 9 | 10 | 11 | class TestFSMPopulation(unittest.TestCase): 12 | name = "score" 13 | turns = 10 14 | noise = 0 15 | repetitions = 5 16 | num_states = 2 17 | opponents = [s() for s in axl.demo_strategies] 18 | size = 10 19 | 20 | objective = dojo.prepare_objective(name=name, 21 | turns=turns, 22 | noise=noise, 23 | repetitions=repetitions) 24 | 25 | def test_generations(self): 26 | temporary_file = tempfile.NamedTemporaryFile() 27 | 28 | params_kwargs = {"parameters": (1, 1, 2)} 29 | population = dojo.Population(player_class=axl.EvolvableGambler, 30 | params_kwargs=params_kwargs, 31 | size=self.size, 32 | objective=self.objective, 33 | output_filename=temporary_file.name, 34 | opponents=self.opponents, 35 | bottleneck=2, 36 | mutation_probability=.01, 37 | processes=1) 38 | 39 | generations = 4 40 | axl.seed(0) 41 | population.run(generations, print_output=False) 42 | self.assertEqual(population.generation, generations) 43 | 44 | results = [] 45 | with open(temporary_file.name, "r") as f: 46 | reader = csv.reader(f) 47 | for row in reader: 48 | results.append(row) 49 | 50 | self.assertEqual(population.generation, len(results)) 51 | 52 | def test_scores(self): 53 | temporary_file = tempfile.NamedTemporaryFile() 54 | params_kwargs = {"parameters": (1, 1, 2)} 55 | population = dojo.Population(player_class=axl.EvolvableGambler, 56 | params_kwargs=params_kwargs, 57 | size=self.size, 58 | objective=self.objective, 59 | output_filename=temporary_file.name, 60 | opponents=self.opponents, 61 | bottleneck=2, 62 | mutation_probability=.01, 63 | processes=1) 64 | 65 | generations = 10 66 | axl.seed(1) 67 | population.run(generations, print_output=False) 68 | 69 | results = [] 70 | with open(temporary_file.name, "r") as f: 71 | reader = csv.reader(f) 72 | for row in reader: 73 | results.append(row) 74 | 75 | self.assertTrue(all([0<=float(result[3]) <= 5 for result in results])) 76 | -------------------------------------------------------------------------------- /src/axelrod_dojo/algorithms/particle_swarm_optimization.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import cpu_count 2 | import axelrod as axl 3 | import pyswarm 4 | from axelrod_dojo.utils import score_player 5 | from axelrod_dojo.utils import PlayerInfo 6 | 7 | 8 | class PSO(object): 9 | """PSO class that implements a particle swarm optimization algorithm.""" 10 | def __init__(self, player_class, params_kwargs, objective, opponents=None, 11 | population=1, generations=1, debug=True, phip=0.8, phig=0.8, 12 | omega=0.8, weights=None, sample_count=None, processes=1): 13 | 14 | self.player_class = player_class 15 | self.params_kwargs = params_kwargs 16 | self.objective = objective 17 | if opponents is None: 18 | self.opponents_information = [ 19 | PlayerInfo(s, {}) for s in axl.short_run_time_strategies] 20 | else: 21 | self.opponents_information = [ 22 | PlayerInfo(p.__class__, p.init_kwargs) for p in opponents] 23 | self.population = population 24 | self.generations = generations 25 | self.debug = debug 26 | self.phip = phip 27 | self.phig = phig 28 | self.omega = omega 29 | self.weights = weights 30 | self.sample_count = sample_count 31 | if processes == 0: 32 | self.processes = cpu_count() 33 | else: 34 | self.processes = processes 35 | 36 | def swarm(self): 37 | player = self.player_class(**self.params_kwargs) 38 | lb, ub = player.create_vector_bounds() 39 | 40 | def objective_function(vector): 41 | player.receive_vector(vector=vector) 42 | 43 | return -score_player(player, objective=self.objective, 44 | opponents_information=self.opponents_information, 45 | weights=self.weights, 46 | sample_count=self.sample_count 47 | ) 48 | 49 | # TODO remove check once v 0.7 is pip installable 50 | # There is a multiprocessing version (0.7) of pyswarm available at 51 | # https://github.com/tisimst/pyswarm, just pass processes=X 52 | # Pip installs version 0.6 53 | if pyswarm.__version__ == "0.7": 54 | xopt, fopt = pyswarm.pso(objective_function, lb, ub, 55 | swarmsize=self.population, 56 | maxiter=self.generations, debug=self.debug, 57 | phip=self.phip, phig=self.phig, 58 | omega=self.omega, processes=self.processes) 59 | else: 60 | xopt, fopt = pyswarm.pso(objective_function, lb, ub, 61 | swarmsize=self.population, 62 | maxiter=self.generations, debug=self.debug, 63 | phip=self.phip, phig=self.phig, 64 | omega=self.omega) 65 | return xopt, fopt 66 | -------------------------------------------------------------------------------- /src/axelrod_dojo/arguments.py: -------------------------------------------------------------------------------- 1 | from docopt import docopt 2 | from .utils import prepare_objective 3 | from .algorithms.evolutionary_algorithm import Population 4 | from .algorithms.particle_swarm_optimization import PSO 5 | 6 | 7 | def parse_arguments(doc, version=None): 8 | arguments = docopt(doc, version=version) 9 | try: 10 | algorithm = arguments["--algorithm"].lower() 11 | except KeyError: 12 | algorithm = "ea" 13 | algorithm_arguments = { 14 | "processes": int(arguments['--processes']), 15 | # Vars for the genetic algorithm 16 | "population": int(arguments['--population']), 17 | "generations": int(arguments['--generations']), 18 | "bottleneck": int(arguments['--bottleneck']), 19 | "mutation_probability": float(arguments['--mu']), 20 | "output_filename": arguments['--output'], 21 | # Objective 22 | "name": str(arguments['--objective']), 23 | "repetitions": int(arguments['--repetitions']), 24 | "turns": int(arguments['--turns']), 25 | "noise": float(arguments['--noise']), 26 | "nmoran": int(arguments['--nmoran']) 27 | } 28 | 29 | objective = prepare_objective( 30 | algorithm_arguments["name"], 31 | algorithm_arguments["turns"], 32 | algorithm_arguments["noise"], 33 | algorithm_arguments["repetitions"], 34 | algorithm_arguments["nmoran"] 35 | ) 36 | 37 | return arguments, algorithm, algorithm_arguments, objective 38 | 39 | 40 | def invoke_training(doc, version, player_class, player_kwargs_func): 41 | arguments, algorithm, algorithm_arguments, objective = parse_arguments(doc, version) 42 | player_kwargs = player_kwargs_func(arguments) 43 | print(arguments) 44 | 45 | # Evolutionary Algorithm 46 | if algorithm == "ea": 47 | population = Population( 48 | player_class, 49 | player_kwargs, 50 | algorithm_arguments["population"], 51 | objective, 52 | algorithm_arguments["output_filename"], 53 | algorithm_arguments["bottleneck"], 54 | algorithm_arguments["mutation_probability"], 55 | processes=algorithm_arguments["processes"]) 56 | 57 | population.run(algorithm_arguments["generations"]) 58 | 59 | # Get the best member of the population to output. 60 | scores = population.score_all() 61 | record, record_holder = 0, -1 62 | for i, s in enumerate(scores): 63 | if s >= record: 64 | record = s 65 | record_holder = i 66 | xopt, fopt = population.population[record_holder], record 67 | 68 | # Particle Swarm Algorithm 69 | elif algorithm == "ps": 70 | pso = PSO(player_class, 71 | player_kwargs, 72 | objective=objective, 73 | population=algorithm_arguments["population"], 74 | generations=algorithm_arguments["generations"] 75 | ) 76 | xopt_helper, fopt = pso.swarm() 77 | xopt = player_class(**player_kwargs) 78 | # xopt.read_vector(xopt_helper, num_states) 79 | 80 | else: 81 | print("Algorithm must be one of EA or PS.") 82 | exit() 83 | 84 | # Print best performer. 85 | print("Best Score: {} {}".format(fopt, xopt)) 86 | -------------------------------------------------------------------------------- /lookup_tables.csv: -------------------------------------------------------------------------------- 1 | # Name (string), plays (int), opp_plays(int), starting_plays(int), pattern (str) 2 | , 0, 0, 1, C, CD 3 | , 0, 0, 2, CC, CDCD 4 | , 0, 0, 3, CCC, CDDDCDDD 5 | , 1, 1, 0, C, CDDD 6 | , 1, 1, 1, C, CDDDDDCD 7 | , 1, 1, 2, CC, CDDDDDDDDCCDCDDD 8 | , 1, 1, 3, CCC, CDDDDDDDDDDDCDDDDCCDCDDDDCDDDDDD 9 | , 1, 1, 4, CCCC, CDCCDDDDCDDDDDCCDCDCDDDDDCDDDDDDDCCDCDCDDDDDCDDDDCDDDDDDCDDDCDDD 10 | , 2, 2, 0, CC, CDDDCDCDDCDDDDDD 11 | , 2, 2, 1, C, CDCCCCDDDCCCDDDCCCDDCDCDDCCCDDDD 12 | , 2, 2, 2, CC, CDCCDCCCDCDDDCCCDCDDDDDDDCDDDCDCDDDDCCDCCCCDDDDCCDDDDCCDCDDDDDDD 13 | , 2, 2, 3, CCC, CDCCDCCCDCDCDDDCCDCCCDCCDDDCDDDDDDCCCDDCDCDCDDDCCDCDCCDDCCCCDDDCDDCDCCDDCCDDDDDDCDDDDDCDCDDDDDDDDCDCDCDCCDDDDDDDCCCDCDDDDCDDDDDD 14 | , 2, 2, 4, CCCC, CDCCDCCCDCDCDDDCCDCCDDCDDCDCDDDCDCCCCDCCDCDCDDDCCCCDCDDDCDCCCDDCDDDCCDCDDCCCDDDCDDDDCDDDDCDCDDDCDDCCCCDDCCCCDCDCCDCDDDCDDCCCDCDCDCCDCCCDCCCCDDDDCDCCDCCDCDDDDDDDDDDCDCCCDDCDDDDDCDCDCCDDDCCDDDDDDDCDDDDDDCCDDDDDCCDDDCCCCCDDDDDDCDCDCDDCDCDDDDDCCCDDCDDDDDCDDDDD 15 | , 3, 3, 1, CCC, CDDDCDCCCCCCDDDDDDCDDCDCDDDCCDDCDDCCDCCCDDDDCCDDDDCCDDCCDDDDDDDCCDCDDDDDCCDDCDDDDCCDCDCCCCCDCDDDDDCCCDCCCCCDDDCDDDDDDCDCDDDDDDDD 16 | , 3, 3, 2, CCC, CDCCCCCCCDCCCDCCDDDDDCDCDDDDCDDCDDCCCCCCDDDDCCCDDDDDDDDCDDDDDDDCCDDDCDCCCCCCCDDDDDCCCCDCDCDCCCDCDCCDDDCCCDDCDDCCDCCCCCDCDDDDDDDDDDDDDDDCDCCDCCCDCCDDDDDDCCDDDCCDCDDCDCDDCCDDDDCDCDDCDCDDDDDDDDDDCDDDCCDDDCCCDCDDDDCDDCCDCCCDCCDDDCDDCCCCDCDCDCDDDDDCCDDCDDDDDDDD 17 | , 3, 3, 3, CCC, CDCCCCCCCCCCDDCDDDCCDCDCDDCDDCCCCDCCCCCCCCDCDCDCDCDCCCDDDDDCDDDDCDCCCCCCCDCDDCDCDDDDDCDCDDDCCCCCDDCCCDCCCDDDDCCCDDDCCCDCDDDDDDDDCDDCCDDCDDCDCDCDCDDCDCDCDDDDDCDDDDCDCDCCDCDDCCCCDCCCDCDDDDDDDDDCCCCDCCCCCDDCCDCDCCCCDCDCDDDDDDDCDCCCDDCCCCDDCDDDCDCDCDCCDDDDDDDDCCCCDCDDCCDDCCDCDCCDDDCDDDDDDCDDDDCCCDDDCDCCDCCDDDCCDCDDCDDDDDDDCDCCDDDDDDDDDCDDDCCDCDCCDDCDCCCDCCDCDCCDDDDCDDDDCDDDDCCDDDDDDDDDCDDDCCDCCCDDDCDDCDDDDDCDDDCDCCDDCDCDDDDDCDDCDDCDDDDCDDDDDDDDDDDDCCCCCDDDDCCCDCDDCDDCCCDCCDCCCDDDCDCDDDDCCDDDDDDCCDCDCDCCDDDDDDDD 18 | , 4, 4, 1, CCCC, CDDDDCDDCDDCCDCCDDCCDDDCDDCCDCDDCCDCCCCCCDCDDDDCDDDDCCDCDDDCDDDCCCCCCCDDDDCCDCCCDDDCCCCDCDDCCDCCCDDDCCCDDDDCDCDDDDDDDDDCCCDDDCCDCDDCCDCCCCCDCDCCCDDDDCCDDDDCDDCDDCDCCCCCCDCDCCDCDDCCCCCCCDDCDDDDDCCCCDDCDCDDCDCCCDDDCCDCDDCCCDDDDCDCDCDCDDDDCDDCDDDDDDDDDDDDDDDCCCCCCCCCDDCCCDCDDDCDCCCDCDCDDCCDCCDCCDDCDDDDCDDDCCDDCCCDDDDDCCDDDDCDDDDCCCDCDCDCDDCDCDDDCCDDDCCDCDCCCDDDDCCDCDDCCDCCCDCCDDDDDCCDDCCDDDCDCDDDDDDDCDDDDCCCDCDDDDCDDCCDCCCDCDDCCCCDDCDCDCDCDDDCDDDDCCDDDCCDDCDCCCCDCCCCCCCDDDDDDDCDCDDCCCCDDCDDCCDCDDDDDDDDDDDDDDDD 19 | , 4, 4, 2, CCCC, CDDDCCCCCDCCCCCCDDDCDCCDCDCCCCDCDDCDCDDCDCCCDDDCDDCCDCDCDDCCDDDCDDCCCDCCCCDDCCCCDDCCCDDCCDCCDDDDCDCCDCDCCDCCDDCDDDDDDCDDDDDCCDCCCCDCCCDCCDCDCCCCCDDCCDDDCCDCCCDDCDCDDCDCDCDDCDDCCCCDCCDDDDDCCDCDDDDCDCDCDDDDDCCCDCDDCDCDDCCDDCCDDCCDCDDCDDDCDCCCDDDCDDDDDDDDDDDCCCDDDDCDCCDDDDDCCDCDCCDCCDDDCCCDDDCDDCDDCCCDCCDCDCDDCDDDCDDDDDDCDDDCDDCCCDDDCCCCDDCDCDCDCCDDCCCDDCDCCDCCDCDCDDCCCDDCDDCCCCDCDDDCCDDDCCDDDCDDCDDDCDDDDDDCCCCCCDCDCCDDCCDDDDCDCCDCCCDCDCCDDCCDCDDDDCDDCCCCCCDDDDCCCCDCCDCCCDCDCCCCDDDCCCCCDDCDDCCCDDDDDDDDDDDDDDDDCCDDDDCDDCCDDDDDCDDDDCDDDCDCDDDDDCCCCCDCDDCDCCCDCCDDDCDDDCDCDDCDDDCDCDDDCCDDDCCDCCCCCCDCCDCDCCDCCDCDCCCDDCDCCCDDCCCCDDDDCCDCDCCDCCCDCCDCCDCCDDCCCDCCDDDDCDDCCCDDCCDDCCDCDDCCCDCDDDDCDCDCDDCDCDDDCCDDCDCCCDCDCCDDDCDCDCDCCDCDDCCCCDCDCCDDDCDCDDDDDDDDDDDDDDDDDDDDCDDDCDCDCCCDCDCDCDCCDDDDDCDDCCCDDDDCDDDDDDDCDDDCCCDCCCCDCCDDDDDDDDDCDDDCDDDDCDCDCCCDDDDCCCDCDCCCDCCCDCDDDCCDDDCDDDDCCCCDCCDCDDDDCCCCDDCDDCDDDCDCDDCDDCDCCDCCDCCDCDCDCCDCCDCCDCCCDDDDDCCDDDDDDCDCCCCCDDCDCCDCDDDDDCDDDDDDCDDDDDDDCDCDDDDDCCDDCCCCDDDDDDDDDDDDDDDD 20 | -------------------------------------------------------------------------------- /tests/algorithms/test_particle_swarm_optimization.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import unittest 3 | import numpy as np 4 | 5 | import axelrod as axl 6 | from axelrod import EvolvableGambler, EvolvableFSMPlayer 7 | import axelrod_dojo as dojo 8 | from axelrod_dojo import prepare_objective 9 | from axelrod_dojo.algorithms.particle_swarm_optimization import PSO 10 | 11 | 12 | class TestPSO(unittest.TestCase): 13 | def test_init_default(self): 14 | params = [1, 1, 1] 15 | objective = prepare_objective('score', 2, 0, 1, nmoran=False) 16 | 17 | pso = PSO(EvolvableGambler, params, objective=objective) 18 | 19 | self.assertIsInstance(pso.objective, functools.partial) 20 | self.assertEqual(len(pso.opponents_information), len(axl.short_run_time_strategies)) 21 | self.assertEqual(pso.population, 1) 22 | self.assertEqual(pso.generations, 1) 23 | self.assertTrue(pso.debug) 24 | self.assertEqual(pso.phip, 0.8) 25 | self.assertEqual(pso.phig, 0.8) 26 | self.assertEqual(pso.omega, 0.8) 27 | self.assertEqual(pso.processes, 1) 28 | 29 | def test_init(self): 30 | params = [2, 1, 1] 31 | objective = prepare_objective('score', 2, 0, 1, nmoran=False) 32 | opponents = [axl.Defector(), axl.Cooperator()] 33 | population = 2 34 | generations = 10 35 | debug = False 36 | phip = 0.6 37 | phig = 0.6 38 | omega = 0.6 39 | 40 | pso = PSO(EvolvableGambler, params, objective=objective, 41 | opponents=opponents, population=population, 42 | generations=generations, debug=debug, phip=phip, phig=phig, 43 | omega=omega) 44 | 45 | self.assertIsInstance(pso.objective, functools.partial) 46 | self.assertEqual(len(pso.opponents_information), len(opponents)) 47 | self.assertEqual(pso.population, population) 48 | self.assertEqual(pso.generations, generations) 49 | self.assertFalse(pso.debug) 50 | self.assertEqual(pso.phip, phip) 51 | self.assertEqual(pso.phig, phig) 52 | self.assertEqual(pso.omega, omega) 53 | self.assertEqual(pso.processes, 1) 54 | 55 | def test_pso_with_gambler(self): 56 | name = "score" 57 | turns = 50 58 | noise = 0 59 | repetitions = 5 60 | num_plays = 1 61 | num_op_plays = 1 62 | num_op_start_plays = 1 63 | params_kwargs = {"parameters": (num_plays, num_op_plays, num_op_start_plays)} 64 | population = 10 65 | generations = 100 66 | opponents = [axl.Cooperator() for _ in range(5)] 67 | 68 | objective = dojo.prepare_objective(name=name, 69 | turns=turns, 70 | noise=noise, 71 | repetitions=repetitions) 72 | 73 | pso = dojo.PSO(EvolvableGambler, params_kwargs, objective=objective, debug=False, 74 | opponents=opponents, population=population, generations=generations) 75 | 76 | axl.seed(0) 77 | opt_vector, opt_objective_value = pso.swarm() 78 | 79 | self.assertTrue(np.allclose(opt_vector, np.array([[ 80 | 0., 0.89327795, 0.04140453, 0.73676965, 0., 0.20622436, 1., 0.68104353]]))) 81 | self.assertEqual(abs(opt_objective_value), 3.56) 82 | 83 | def test_pso_with_fsm(self): 84 | name = "score" 85 | turns = 10 86 | noise = 0 87 | repetitions = 5 88 | num_states = 4 89 | params_kwargs = {"num_states": num_states} 90 | population = 10 91 | generations = 100 92 | opponents = [axl.Defector() for _ in range(5)] 93 | 94 | objective = dojo.prepare_objective(name=name, 95 | turns=turns, 96 | noise=noise, 97 | repetitions=repetitions) 98 | 99 | pso = PSO(EvolvableFSMPlayer, params_kwargs, objective=objective, debug=False, 100 | opponents=opponents, population=population, generations=generations) 101 | 102 | axl.seed(0) 103 | opt_vector, opt_objective_value = pso.swarm() 104 | 105 | self.assertTrue(np.allclose( 106 | opt_vector, 107 | np.array([ 108 | 0.22825439, 0.06954976, 0.49462006, 0.27704876, 1., 0.81240316, 0.11818378, 0., 0.4289995, 109 | 0.91397724, 1., 0.7404604, 0.35865552, 1., 0.53483268, 0.41643427, 0.71756716]))) 110 | self.assertEqual(abs(opt_objective_value), 1) 111 | -------------------------------------------------------------------------------- /src/axelrod_dojo/algorithms/evolutionary_algorithm.py: -------------------------------------------------------------------------------- 1 | from itertools import repeat, starmap 2 | from multiprocessing import Pool, cpu_count 3 | from operator import itemgetter 4 | from random import randrange 5 | from statistics import mean, pstdev 6 | 7 | import axelrod as axl 8 | from axelrod_dojo.utils import Outputer, PlayerInfo, score_player 9 | 10 | 11 | class Population(object): 12 | """Population class that implements the evolutionary algorithm.""" 13 | 14 | def __init__(self, player_class, params_kwargs, size, objective, output_filename, 15 | bottleneck=None, mutation_probability=.1, opponents=None, 16 | processes=1, weights=None, 17 | sample_count=None, population=None, print_output=True): 18 | self.print_output = print_output 19 | self.player_class = player_class 20 | self.bottleneck = bottleneck 21 | if processes == 0: 22 | self.processes = cpu_count() 23 | else: 24 | self.processes = processes 25 | 26 | self.pool = Pool(processes=self.processes) 27 | 28 | self.outputer = Outputer(output_filename, mode='a') 29 | self.size = size 30 | self.objective = objective 31 | if not bottleneck: 32 | self.bottleneck = size // 4 33 | else: 34 | self.bottleneck = bottleneck 35 | if opponents is None: 36 | self.opponents_information = [ 37 | PlayerInfo(s, {}) for s in axl.short_run_time_strategies] 38 | else: 39 | self.opponents_information = [ 40 | PlayerInfo(p.__class__, p.init_kwargs) for p in opponents] 41 | self.generation = 0 42 | 43 | self.params_kwargs = params_kwargs 44 | if "mutation_probability" not in self.params_kwargs: 45 | self.params_kwargs["mutation_probability"] = mutation_probability 46 | 47 | if population is not None: 48 | self.population = population 49 | else: 50 | self.population = [player_class(**params_kwargs) for _ in range(self.size)] 51 | 52 | self.weights = weights 53 | self.sample_count = sample_count 54 | 55 | def score_all(self): 56 | starmap_params_zip = zip( 57 | self.population, 58 | repeat(self.objective), 59 | repeat(self.opponents_information), 60 | repeat(self.weights), 61 | repeat(self.sample_count)) 62 | if self.processes == 1: 63 | results = list(starmap(score_player, starmap_params_zip)) 64 | else: 65 | results = self.pool.starmap(score_player, starmap_params_zip) 66 | return results 67 | 68 | def subset_population(self, indices): 69 | population = [] 70 | for i in indices: 71 | population.append(self.population[i]) 72 | self.population = population 73 | 74 | @staticmethod 75 | def crossover(population, num_variants): 76 | new_variants = [] 77 | for _ in range(num_variants): 78 | i = randrange(len(population)) 79 | j = randrange(len(population)) 80 | new_variant = population[i].crossover(population[j]) 81 | new_variants.append(new_variant) 82 | return new_variants 83 | 84 | def evolve(self): 85 | self.generation += 1 86 | if self.print_output: 87 | print("Scoring Generation {}".format(self.generation)) 88 | 89 | # Score population 90 | scores = self.score_all() 91 | results = list(zip(scores, range(len(scores)))) 92 | results.sort(key=itemgetter(0), reverse=True) 93 | 94 | # Report 95 | if self.print_output: 96 | print("Generation", self.generation, "| Best Score:", results[0][0]) 97 | 98 | # Write the data 99 | # Note: if using this for analysis, for reproducibility it may be useful to 100 | # pass type(opponent) for each of the opponents. This will allow verification of results post run 101 | 102 | row = [self.generation, mean(scores), pstdev(scores), results[0][0], 103 | self.player_class.serialize_parameters(self.population[results[0][1]])] 104 | self.outputer.write_row(row) 105 | 106 | # Next Population 107 | indices_to_keep = [p for (s, p) in results[0: self.bottleneck]] 108 | 109 | self.subset_population(indices_to_keep) 110 | # Add mutants of the best players 111 | best_mutants = [p.clone() for p in self.population] 112 | for p in best_mutants: 113 | self.population.append(p.mutate()) 114 | # Add random variants 115 | mutants = [self.player_class(**self.params_kwargs) 116 | for _ in range(self.bottleneck // 2)] 117 | players_to_modify = [player.clone() for player in self.population] 118 | players_to_modify += mutants 119 | # Crossover 120 | size_left = self.size - len(self.population) 121 | players_to_modify = self.crossover(players_to_modify, size_left) 122 | # Mutate 123 | players_to_modify = [p.mutate() for p in players_to_modify] 124 | self.population += players_to_modify 125 | 126 | def __iter__(self): 127 | return self 128 | 129 | def __next__(self): 130 | self.evolve() 131 | 132 | def run(self, generations, print_output=True): 133 | self.print_output = print_output 134 | 135 | for _ in range(generations): 136 | next(self) 137 | -------------------------------------------------------------------------------- /src/axelrod_dojo/utils.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import csv 3 | from functools import partial 4 | from statistics import mean 5 | 6 | import numpy as np 7 | import axelrod as axl 8 | 9 | 10 | PlayerInfo = namedtuple('PlayerInfo', ['strategy', 'init_kwargs']) 11 | 12 | 13 | # Output Evolutionary Algorithm results 14 | 15 | class Outputer(object): 16 | def __init__(self, filename, mode='a'): 17 | self.file = filename 18 | self.mode = mode 19 | 20 | def write_row(self, row): 21 | with open(self.file, self.mode, newline='') as file_writer: 22 | writer = csv.writer(file_writer) 23 | writer.writerow(row) 24 | 25 | 26 | # Objective functions for optimization 27 | 28 | def prepare_objective(name="score", turns=200, noise=0., repetitions=None, 29 | nmoran=None, match_attributes=None): 30 | name = name.lower() 31 | if name not in ["score", "score_diff", "moran"]: 32 | raise ValueError("Score must be one of score, score_diff, or moran") 33 | if name == "moran": 34 | if repetitions is None: 35 | repetitions = 1000 36 | if nmoran is None: 37 | nmoran = 4 38 | objective = partial(objective_moran_win, turns=turns, noise=noise, 39 | repetitions=repetitions, N=nmoran, 40 | match_attributes=match_attributes) 41 | elif name == "score": 42 | if repetitions is None: 43 | repetitions = 20 44 | objective = partial(objective_score, turns=turns, noise=noise, 45 | repetitions=repetitions, 46 | match_attributes=match_attributes) 47 | elif name == "score_diff": 48 | if repetitions is None: 49 | repetitions = 20 50 | objective = partial(objective_score_diff, turns=turns, noise=noise, 51 | repetitions=repetitions, 52 | match_attributes=match_attributes) 53 | return objective 54 | 55 | 56 | def objective_score(me, other, turns, noise, repetitions, match_attributes=None): 57 | """Objective function to maximize total score over matches.""" 58 | match = axl.Match((me, other), turns=turns, noise=noise, 59 | match_attributes=match_attributes) 60 | if not match._stochastic: 61 | repetitions = 1 62 | scores_for_this_opponent = [] 63 | 64 | for _ in range(repetitions): 65 | match.play() 66 | scores_for_this_opponent.append(match.final_score_per_turn()[0]) 67 | return scores_for_this_opponent 68 | 69 | 70 | def objective_score_diff(me, other, turns, noise, repetitions, 71 | match_attributes=None): 72 | """Objective function to maximize total score difference over matches.""" 73 | match = axl.Match((me, other), turns=turns, noise=noise, 74 | match_attributes=match_attributes) 75 | if not match._stochastic: 76 | repetitions = 1 77 | scores_for_this_opponent = [] 78 | 79 | for _ in range(repetitions): 80 | match.play() 81 | final_scores = match.final_score_per_turn() 82 | score_diff = final_scores[0] - final_scores[1] 83 | scores_for_this_opponent.append(score_diff) 84 | return scores_for_this_opponent 85 | 86 | 87 | def objective_moran_win(me, other, turns, noise, repetitions, N=5, 88 | match_attributes=None): 89 | """Objective function to maximize Moran fixations over N=4 matches""" 90 | population = [] 91 | for _ in range(N): 92 | population.append(me.clone()) 93 | population.append(other.clone()) 94 | mp = axl.MoranProcess(population, turns=turns, noise=noise) 95 | 96 | scores_for_this_opponent = [] 97 | 98 | for _ in range(repetitions): 99 | mp.reset() 100 | mp.play() 101 | if mp.winning_strategy_name == str(me): 102 | scores_for_this_opponent.append(1) 103 | else: 104 | scores_for_this_opponent.append(0) 105 | return scores_for_this_opponent 106 | 107 | 108 | def score_player(player, objective, opponents_information, weights=None, sample_count=None): 109 | """ 110 | Return the overall mean score of a Player 111 | """ 112 | scores_for_all_opponents = [] 113 | 114 | if sample_count is not None: 115 | indices = np.random.choice(len(opponents_information), sample_count) 116 | opponents_information = [opponents_information[i] for i in indices] 117 | if weights is not None: 118 | weights = [weights[i] for i in indices] 119 | 120 | for strategy, init_kwargs in opponents_information: 121 | player.reset() 122 | opponent = strategy(**init_kwargs) 123 | scores_for_this_opponent = objective(player, opponent) 124 | mean_vs_opponent = mean(scores_for_this_opponent) 125 | scores_for_all_opponents.append(mean_vs_opponent) 126 | 127 | overall_mean_score = np.average(scores_for_all_opponents, weights=weights) 128 | return overall_mean_score 129 | 130 | 131 | def load_params(player_class, filename, num): 132 | """Load the best num parameters from the given file.""" 133 | parser = player_class.deserialize_parameters 134 | all_params = [] 135 | 136 | with open(filename) as datafile: 137 | reader = csv.reader(datafile) 138 | for line in reader: 139 | score, rep = float(line[-2]), line[-1] 140 | all_params.append((score, rep)) 141 | all_params.sort(reverse=True) 142 | best_params = [] 143 | for score, rep in all_params[:num]: 144 | best_params.append(parser(rep)) 145 | return best_params 146 | -------------------------------------------------------------------------------- /docs/background/genetic_algorithm.rst: -------------------------------------------------------------------------------- 1 | Genetic Algorithm 2 | ================= 3 | 4 | A genetic algorithm aims to mimic evolutionary processes so as to optimise a 5 | particular function on some space of candidate solutions. 6 | 7 | The process can be described by assuming that there is a function 8 | :math:`f:V\to \mathbb{R}`, where :math:`V` is some vector space. 9 | In the case of the Prisoner's dilemma, 10 | the vector space :math:`V` corresponds to some representation of a 11 | particular archetype (which might not actually be a numeric vector space) and 12 | the function :math:`f` corresponds to some measure of performance/fitness of the 13 | strategy in question. 14 | 15 | In this setting a candidate solution :math:`x\in\mathbb{R}^m` corresponds to a 16 | chromosome with each :math:`x_i` corresponding to a gene. 17 | 18 | The genetic algorithm has three essential parameters: 19 | 20 | - The population size: the algorithm makes use of a number of candidate 21 | solutions at each stage. 22 | - The bottle neck parameter: at every stage the candidates in the population are 23 | ranked according to their fitness, only a certain number are kept (the best 24 | performing ones) from one generation to the next. This number is referred to 25 | as the bottle neck. 26 | - The mutation probability: from one stage to the next when new individuals are 27 | added to the population (more about this process shortly) there is a 28 | probability with which each gene randomly mutates. 29 | 30 | New individuals are added to the population (so as to ensure that the population 31 | size stays constant from one stage to the next) using a process of "crossover". 32 | Two high performing individuals are paired and according to some predefined 33 | procedure, genes from both these individuals are combined to create a new 34 | individual. 35 | 36 | For each strategy archetype, this library thus defines a process for mutation as 37 | well as for crossover. 38 | 39 | Finite state machines 40 | --------------------- 41 | 42 | A finite state machine is made up of the following: 43 | 44 | - a mapping from a state/action pair to another target state/action pair 45 | - an initial state/action pair. 46 | 47 | (See [Harper2017]_ for more details.) 48 | 49 | The crossover and mutation are implemented in the following way: 50 | 51 | - Crossover: this is done by taking a randomly selected number of target 52 | state/actions 53 | pairs from one individual and the rest from the other. 54 | - Mutation: given a mutation probability :math:`\delta` each target state/action 55 | has a probability :math:`\delta` of being randomly changed to one of the other 56 | states or actions. Furthermore the **initial** action has a probability of 57 | being swapped of :math:`\delta\times 10^{-1}` and the **initial** state has a 58 | probability of being changed to another random state of :math:`\delta \times 59 | 10^{-1} \times N` (where :math:`N` is the number of states). 60 | 61 | Hidden Markov models 62 | --------------------- 63 | 64 | A hidden Markov model is made up of the following: 65 | 66 | - a mapping from a state/action pair to a probability of defect or cooperation. 67 | - a cooperation transition matrix, the probability of transitioning to each 68 | state, given current state and an opponent cooperation. 69 | - a defection transition matrix, the probability of transitioning to each 70 | state, given current state and an opponent defection. 71 | - an initial state/action pair. 72 | 73 | (See [Harper2017]_ for more details.) 74 | 75 | The crossover and mutation are implemented in the following way: 76 | 77 | - Crossover: this is done by taking a randomly selected number of rows from 78 | one cooperation transition matrix and the rest from the other to form a target 79 | cooperation transition matrix; then a different number of randomly selected 80 | rows from one defection transition matrix and the rest from the other; and 81 | then a randomly select number of entries from one state/part -> probability 82 | mapping and the rest from the other. 83 | - Mutation: given a mutation probability :math:`delta` each cell of both 84 | transition matrices and the state/part -> probability mapping have probability 85 | :math:`delta` of being increased by :math:`varepsilon`, where 86 | :math:`varepsilon` is randomly drawn uniformly from :math:`[-0.25, 0.25]` 87 | (A negative number would decrease.) Then the transition matrices and mapping 88 | are adjusted so that no cell is outside :math:`[0, 1]` and the transition 89 | matrices are normalized so that each row adds to 1. Furthermore the 90 | **initial** action has a probability of being swapped of 91 | :math:`\delta\times 10^{-1}` and the **initial** state has a probability of 92 | being changed to another random state of 93 | :math:`\delta \times 10^{-1} \times N` (where :math:`N` is the number of 94 | states). 95 | 96 | Cycler Sequence Calculator 97 | -------------------------- 98 | 99 | A Cycler Sequence is the sequence of C & D actions that are passed to the cycler player to follow when playing their 100 | tournament games. 101 | 102 | the sequence is found using genetic feature selection: 103 | 104 | - Crossover: By working with another cycler player, we take sections of each player and create a new cycler sequence 105 | from the following formula: 106 | let our two player being crossed be called p1 and p2 respectively. we then find the midpoint of both the sequences 107 | and take the first half from p1 and the second half from p2 to combine into the new cycler sequence. 108 | 109 | - Mutation: we use a predictor :math:`\delta`to determine if we are going to mutate a 110 | single element in the current sequence. The element, or gene, we change in the sequence is uniformly selected using 111 | the random :code:`package`. 112 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Axelrod-dojo documentation build configuration file, created by 5 | # sphinx-quickstart on Wed Oct 11 09:40:19 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.mathjax'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'Axelrod-dojo' 50 | copyright = '2017, The Axelrod project developers' 51 | author = 'The Axelrod project developers' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This patterns also effect to html_static_path and html_extra_path 72 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | # If true, `todo` and `todoList` produce output, else they produce nothing. 78 | todo_include_todos = False 79 | 80 | 81 | # -- Options for HTML output ---------------------------------------------- 82 | 83 | # The theme to use for HTML and HTML Help pages. See the documentation for 84 | # a list of builtin themes. 85 | import os 86 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 87 | 88 | if not on_rtd: # only import and set the theme if we're building docs locally 89 | import sphinx_rtd_theme 90 | html_theme = 'sphinx_rtd_theme' 91 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 92 | 93 | # Theme options are theme-specific and customize the look and feel of a theme 94 | # further. For a list of options available for each theme, see the 95 | # documentation. 96 | # 97 | # html_theme_options = {} 98 | 99 | # Add any paths that contain custom static files (such as style sheets) here, 100 | # relative to this directory. They are copied after the builtin static files, 101 | # so a file named "default.css" will overwrite the builtin "default.css". 102 | html_static_path = ['_static'] 103 | 104 | # Custom sidebar templates, must be a dictionary that maps document names 105 | # to template names. 106 | # 107 | # This is required for the alabaster theme 108 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 109 | html_sidebars = { 110 | '**': [ 111 | 'about.html', 112 | 'navigation.html', 113 | 'relations.html', # needs 'show_related': True theme option to display 114 | 'searchbox.html', 115 | 'donate.html', 116 | ] 117 | } 118 | 119 | 120 | # -- Options for HTMLHelp output ------------------------------------------ 121 | 122 | # Output file base name for HTML help builder. 123 | htmlhelp_basename = 'Axelrod-dojodoc' 124 | 125 | 126 | # -- Options for LaTeX output --------------------------------------------- 127 | 128 | latex_elements = { 129 | # The paper size ('letterpaper' or 'a4paper'). 130 | # 131 | # 'papersize': 'letterpaper', 132 | 133 | # The font size ('10pt', '11pt' or '12pt'). 134 | # 135 | # 'pointsize': '10pt', 136 | 137 | # Additional stuff for the LaTeX preamble. 138 | # 139 | # 'preamble': '', 140 | 141 | # Latex figure (float) alignment 142 | # 143 | # 'figure_align': 'htbp', 144 | } 145 | 146 | # Grouping the document tree into LaTeX files. List of tuples 147 | # (source start file, target name, title, 148 | # author, documentclass [howto, manual, or own class]). 149 | latex_documents = [ 150 | (master_doc, 'Axelrod-dojo.tex', 'Axelrod-dojo Documentation', 151 | 'The Axelrod project developers', 'manual'), 152 | ] 153 | 154 | 155 | # -- Options for manual page output --------------------------------------- 156 | 157 | # One entry per manual page. List of tuples 158 | # (source start file, name, description, authors, manual section). 159 | man_pages = [ 160 | (master_doc, 'axelrod-dojo', 'Axelrod-dojo Documentation', 161 | [author], 1) 162 | ] 163 | 164 | 165 | # -- Options for Texinfo output ------------------------------------------- 166 | 167 | # Grouping the document tree into Texinfo files. List of tuples 168 | # (source start file, target name, title, author, 169 | # dir menu entry, description, category) 170 | texinfo_documents = [ 171 | (master_doc, 'Axelrod-dojo', 'Axelrod-dojo Documentation', 172 | author, 'Axelrod-dojo', 'One line description of project.', 173 | 'Miscellaneous'), 174 | ] 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import tempfile 4 | import functools 5 | 6 | import axelrod as axl 7 | import axelrod_dojo.utils as utils 8 | 9 | 10 | class TestOutputer(unittest.TestCase): 11 | temporary_file = tempfile.NamedTemporaryFile() 12 | outputer = utils.Outputer(filename=temporary_file.name, mode='a') 13 | 14 | def test_init(self): 15 | self.assertEqual(self.outputer.file, self.temporary_file.name) 16 | self.assertEqual(self.outputer.mode, 'a') 17 | 18 | def test_write_and_clear(self): 19 | writing_line = [1, "something", 3.0] 20 | self.outputer.write_row(writing_line) 21 | self.outputer.write_row(writing_line) 22 | with open(self.temporary_file.name, "r") as f: 23 | self.assertEqual("1,something,3.0\n1,something,3.0\n", f.read()) 24 | 25 | 26 | class TestPrepareObjective(unittest.TestCase): 27 | def test_incorrect_objective_name(self): 28 | name = "not_correct_name" 29 | with self.assertRaises(ValueError): 30 | utils.prepare_objective(name=name) 31 | 32 | def test_score(self): 33 | objective = utils.prepare_objective( 34 | name="score", 35 | turns=200, 36 | noise=0, 37 | match_attributes={"length": float("inf")}, 38 | repetitions=5) 39 | self.assertIsInstance(objective, functools.partial) 40 | self.assertIn("objective_score ", str(objective)) 41 | 42 | def test_score_diff(self): 43 | objective = utils.prepare_objective(name="score_diff", 44 | turns=200, 45 | noise=0, 46 | repetitions=5) 47 | self.assertIsInstance(objective, functools.partial) 48 | self.assertIn("objective_score_diff ", str(objective)) 49 | 50 | def test_moran(self): 51 | objective = utils.prepare_objective(name="moran", 52 | turns=200, 53 | noise=0, 54 | repetitions=5) 55 | self.assertIsInstance(objective, functools.partial) 56 | self.assertIn("objective_moran_win ", str(objective)) 57 | 58 | 59 | class TestObjectiveScore(unittest.TestCase): 60 | def test_deterministic_player_opponent(self): 61 | player = axl.TitForTat() 62 | opponent = axl.Defector() 63 | expected_scores = [1 / 2] 64 | scores = utils.objective_score(player, 65 | opponent, 66 | turns=2, 67 | repetitions=5, 68 | noise=0) 69 | self.assertEqual(expected_scores, scores) 70 | 71 | player = axl.TitForTat() 72 | opponent = axl.Alternator() 73 | expected_scores = [8 / 3] 74 | scores = utils.objective_score(player, 75 | opponent, 76 | turns=3, 77 | repetitions=5, 78 | noise=0) 79 | self.assertEqual(expected_scores, scores) 80 | 81 | def test_noisy_match(self): 82 | axl.seed(0) 83 | player = axl.Cooperator() 84 | opponent = axl.Defector() 85 | scores = utils.objective_score(player, 86 | opponent, 87 | turns=2, 88 | repetitions=3, 89 | noise=.8) 90 | # Stochastic so more repetitions are run 91 | self.assertEqual(len(scores), 3) 92 | # Cooperator should score 0 but noise implies it scores more 93 | self.assertNotEqual(max(scores), 0) 94 | 95 | 96 | class TestObjectiveScoreDiff(unittest.TestCase): 97 | def test_deterministic_player_opponent(self): 98 | player = axl.TitForTat() 99 | opponent = axl.Defector() 100 | expected_score_diffs = [1 / 2 - 6 / 2] 101 | score_diffs = utils.objective_score_diff(player, 102 | opponent, 103 | turns=2, 104 | repetitions=5, 105 | noise=0) 106 | self.assertEqual(expected_score_diffs, score_diffs) 107 | 108 | player = axl.TitForTat() 109 | opponent = axl.Alternator() 110 | expected_score_diffs = [8 / 3 - 8 / 3] 111 | score_diffs = utils.objective_score_diff(player, 112 | opponent, 113 | turns=3, 114 | repetitions=5, 115 | noise=0) 116 | self.assertEqual(expected_score_diffs, score_diffs) 117 | 118 | def test_noisy_match(self): 119 | axl.seed(0) 120 | player = axl.Cooperator() 121 | opponent = axl.Defector() 122 | score_diffs = utils.objective_score_diff(player, 123 | opponent, 124 | turns=2, 125 | repetitions=3, 126 | noise=.8) 127 | # Stochastic so more repetitions are run 128 | self.assertEqual(len(score_diffs), 3) 129 | # Cooperator should score 0 (for a diff of -5) 130 | # but noise implies it scores more 131 | self.assertNotEqual(max(score_diffs), -5) 132 | 133 | 134 | class TestObjectiveMoran(unittest.TestCase): 135 | def test_deterministic_cooperator_never_fixes(self): 136 | player = axl.Cooperator() 137 | opponent = axl.Defector() 138 | expected_fixation_probabilities = [0 for _ in range(5)] 139 | fixation_probabilities = utils.objective_moran_win(player, 140 | opponent, 141 | turns=2, 142 | repetitions=5, 143 | noise=0) 144 | self.assertEqual(fixation_probabilities, 145 | expected_fixation_probabilities) 146 | 147 | def test_stochastic_outcomes(self): 148 | for seed, expected in zip(range(2), [[0, 1, 0, 1, 0], [0, 0, 0, 1, 1]]): 149 | axl.seed(seed) 150 | player = axl.TitForTat() 151 | opponent = axl.Defector() 152 | expected_fixation_probabilities = expected 153 | fixation_probabilities = utils.objective_moran_win(player, 154 | opponent, 155 | turns=3, 156 | repetitions=5, 157 | noise=0) 158 | self.assertEqual(fixation_probabilities, 159 | expected_fixation_probabilities) 160 | 161 | 162 | class TestScoreParams(unittest.TestCase): 163 | def test_score(self): 164 | axl.seed(0) 165 | opponents_information = [utils.PlayerInfo(s, {}) 166 | for s in axl.demo_strategies] 167 | objective = utils.prepare_objective() 168 | score = utils.score_player(axl.Cooperator(), 169 | objective=objective, 170 | opponents_information=opponents_information) 171 | expected_score = 2.0949 172 | self.assertEqual(score, expected_score) 173 | 174 | def test_with_init_kwargs(self): 175 | axl.seed(0) 176 | opponents_information = [utils.PlayerInfo(axl.Random, {"p": 0})] 177 | objective = utils.prepare_objective() 178 | score = utils.score_player(axl.Cooperator(), 179 | objective=objective, 180 | opponents_information=opponents_information) 181 | expected_score = 0 182 | self.assertEqual(score, expected_score) 183 | 184 | opponents_information = [utils.PlayerInfo(axl.Random, {"p": 1})] 185 | objective = utils.prepare_objective() 186 | score = utils.score_player(axl.Cooperator(), 187 | objective=objective, 188 | opponents_information=opponents_information) 189 | expected_score = 3.0 190 | self.assertEqual(score, expected_score) 191 | 192 | def test_score_with_weights(self): 193 | axl.seed(0) 194 | opponents_information = [utils.PlayerInfo(s, {}) 195 | for s in axl.demo_strategies] 196 | objective = utils.prepare_objective() 197 | score = utils.score_player(axl.Cooperator(), 198 | objective=objective, 199 | opponents_information=opponents_information, 200 | # All weight on Coop 201 | weights=[1, 0, 0, 0, 0]) 202 | expected_score = 3 203 | self.assertEqual(score, expected_score) 204 | 205 | score = utils.score_player(axl.Cooperator(), 206 | objective=objective, 207 | opponents_information=opponents_information, 208 | # Shared weight between Coop and Def 209 | weights=[2, 2, 0, 0, 0]) 210 | expected_score = 1.5 211 | self.assertEqual(score, expected_score) 212 | 213 | score = utils.score_player(axl.Cooperator(), 214 | objective=objective, 215 | opponents_information=opponents_information, 216 | # Shared weight between Coop and Def 217 | weights=[2, -.5, 0, 0, 0]) 218 | expected_score = 4.0 219 | self.assertEqual(score, expected_score) 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Coverage 2 | Status](https://coveralls.io/repos/github/Axelrod-Python/axelrod-dojo/badge.svg?branch=packaging)](https://coveralls.io/github/Axelrod-Python/axelrod-dojo?branch=packaging) 3 | 4 | [![Build 5 | Status](https://travis-ci.org/Axelrod-Python/axelrod-dojo.svg?branch=master)](https://travis-ci.org/Axelrod-Python/axelrod-dojo) 6 | 7 | # Axelrod Evolvers 8 | 9 | 10 | This repository contains reinforcement learning training code for the following 11 | strategy types: 12 | 13 | * Lookup tables (LookerUp) 14 | * Particle Swarm algorithms (PSOGambler), a stochastic version of LookerUp 15 | * Feed Forward Neural Network (EvolvedANN) 16 | * Finite State Machine (FSMPlayer) 17 | * Hidden Markov Models (HMMPLayer), essentially a stochastic version of a finite state machine 18 | 19 | Model training is by [evolutionary algorithms](https://en.wikipedia.org/wiki/Evolutionary_algorithm) 20 | or [particle swarm algorithms](https://en.wikipedia.org/wiki/Particle_swarm_optimization). 21 | There is another repository in the [Axelrod project](https://github.com/Axelrod-Python/Axelrod) 22 | that trains Neural Networks with gradient descent (using tensorflow) 23 | that will likely be incorporated here. 24 | 25 | You can use this as a library. 26 | 27 | ## Development 28 | 29 | Install a development version of the library: 30 | 31 | $ python setup.py develop 32 | 33 | Run the tests: 34 | 35 | $ python -m unittest discover tests 36 | 37 | ## Scripts 38 | 39 | In this repository there are scripts 40 | for each strategy type with a similar interface: 41 | 42 | * `looker_evolve.py` 43 | * `pso_evolve.py` 44 | * `ann_evolve.py` 45 | * `fsm_evolve.py` 46 | * `hmm_evolve.py` 47 | 48 | 49 | See below for usage instructions. 50 | 51 | In the original iteration the strategies were run against all the default 52 | strategies in the Axelrod library. This is slow and probably not necessary. 53 | By default the training strategies are the `short_run_time_strategies` from the 54 | Axelrod library. You may specify any other set of strategies for training. 55 | 56 | Basic testing is done by running the trained model against the full set of 57 | strategies in various tournaments. Depending on the optimization function 58 | testing methods will vary. 59 | 60 | ## The Strategies 61 | 62 | **LookerUp** is based on lookup tables with three parameters: 63 | * n1, the number of rounds of trailing history to use 64 | * n2, the number of rounds of trailing opponent history to use 65 | * m, the number of rounds of initial opponent play to use 66 | 67 | These are generalizations of deterministic memory-N strategies. 68 | 69 | **PSOGambler** is a stochastic version of LookerUp, trained with a particle 70 | swarm algorithm. The resulting strategies are generalizations of memory-N 71 | strategies. 72 | 73 | **EvolvedANN** is based on a [feed forward neural network](https://en.wikipedia.org/wiki/Feedforward_neural_network) 74 | with a single hidden layer. Various features are derived from the history of play. 75 | The number of nodes in the hidden layer can also be changed. 76 | 77 | **EvolvedFSM** searches over [finite state machines](https://en.wikipedia.org/wiki/Finite-state_machine) 78 | with a given number of states. 79 | 80 | **EvolvedHMM** implements a simple [hidden markov model](https://en.wikipedia.org/wiki/Hidden_Markov_model) 81 | based strategy, a stochastic finite state machine. 82 | 83 | Note that large values of some parameters will make the strategies prone to 84 | overfitting. 85 | 86 | ## Optimization Functions 87 | 88 | There are three objective functions: 89 | * Maximize mean match score over all opponents with `objective_score` 90 | * Maximize mean match score difference over all opponents with `objective_score_difference` 91 | * Maximize Moran process fixation probability with `objective_moran_win` 92 | 93 | Parameters for the objective functions can be specified in the command line 94 | arguments for each evolver. 95 | 96 | ## Running 97 | 98 | ### Look up Tables 99 | 100 | ```bash 101 | $ python lookup_evolve.py -h 102 | Lookup Table Evolver 103 | 104 | Usage: 105 | lookup_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION] 106 | [--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSORS] 107 | [--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS] 108 | [--turns TURNS] [--noise NOISE] [--nmoran NMORAN] 109 | [--plays PLAYS] [--op_plays OP_PLAYS] [--op_start_plays OP_START_PLAYS] 110 | 111 | Options: 112 | -h --help Show help 113 | --generations GENERATIONS Generations to run the EA [default: 500] 114 | --population POPULATION Starting population size [default: 10] 115 | --mu MUTATION_RATE Mutation rate [default: 0.1] 116 | --bottleneck BOTTLENECK Number of individuals to keep from each generation [default: 5] 117 | --processes PROCESSES Number of processes to use [default: 1] 118 | --output OUTPUT_FILE File to write data to [default: lookup_tables.csv] 119 | --objective OBJECTIVE Objective function [default: score] 120 | --repetitions REPETITIONS Repetitions in objective [default: 100] 121 | --turns TURNS Turns in each match [default: 200] 122 | --noise NOISE Match noise [default: 0.00] 123 | --nmoran NMORAN Moran Population Size, if Moran objective [default: 4] 124 | --plays PLAYS Number of recent plays in the lookup table [default: 2] 125 | --op_plays OP_PLAYS Number of recent plays in the lookup table [default: 2] 126 | --op_start_plays OP_START_PLAYS Number of opponent starting plays in the lookup table [default: 2] 127 | ``` 128 | 129 | ### Particle Swarm 130 | 131 | ```bash 132 | $ python pso_evolve.py -h 133 | Particle Swarm strategy training code. 134 | 135 | Original version by Georgios Koutsovoulos @GDKO : 136 | https://gist.github.com/GDKO/60c3d0fd423598f3c4e4 137 | Based on Martin Jones @mojones original LookerUp code 138 | 139 | Usage: 140 | pso_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION] 141 | [--processes PROCESSORS] [--output OUTPUT_FILE] [--objective OBJECTIVE] 142 | [--repetitions REPETITIONS] [--turns TURNS] [--noise NOISE] 143 | [--nmoran NMORAN] 144 | [--plays PLAYS] [--op_plays OP_PLAYS] [--op_start_plays OP_START_PLAYS] 145 | 146 | Options: 147 | -h --help Show help 148 | --population POPULATION Starting population size [default: 10] 149 | --processes PROCESSES Number of processes to use [default: 1] 150 | --output OUTPUT_FILE File to write data to [default: pso_tables.csv] 151 | --objective OBJECTIVE Objective function [default: score] 152 | --repetitions REPETITIONS Repetitions in objective [default: 100] 153 | --turns TURNS Turns in each match [default: 200] 154 | --noise NOISE Match noise [default: 0.00] 155 | --nmoran NMORAN Moran Population Size, if Moran objective [default: 4] 156 | --plays PLAYS Number of recent plays in the lookup table [default: 2] 157 | --op_plays OP_PLAYS Number of recent plays in the lookup table [default: 2] 158 | --op_start_plays OP_START_PLAYS Number of opponent starting plays in the lookup table [default: 2] 159 | ``` 160 | 161 | Note that to use the multiprocessor version you'll need to install pyswarm 0.70 162 | directly (pip installs 0.60 which lacks mutiprocessing support). 163 | 164 | ### Neural Network 165 | 166 | ```bash 167 | $ python ann_evolve.py -h 168 | ANN evolver. 169 | Trains ANN strategies with an evolutionary algorithm. 170 | 171 | Original version by Martin Jones @mojones: 172 | https://gist.github.com/mojones/b809ba565c93feb8d44becc7b93e37c6 173 | 174 | Usage: 175 | ann_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION] 176 | [--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSORS] 177 | [--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS] 178 | [--turns TURNS] [--noise NOISE] [--nmoran NMORAN] 179 | [--features FEATURES] [--hidden HIDDEN] [--mu_distance DISTANCE] 180 | 181 | Options: 182 | -h --help Show help 183 | --generations GENERATIONS Generations to run the EA [default: 500] 184 | --population POPULATION Starting population size [default: 10] 185 | --mu MUTATION_RATE Mutation rate [default: 0.1] 186 | --bottleneck BOTTLENECK Number of individuals to keep from each generation [default: 5] 187 | --processes PROCESSES Number of processes to use [default: 1] 188 | --output OUTPUT_FILE File to write data to [default: ann_weights.csv] 189 | --objective OBJECTIVE Objective function [default: score] 190 | --repetitions REPETITIONS Repetitions in objective [default: 100] 191 | --turns TURNS Turns in each match [default: 200] 192 | --noise NOISE Match noise [default: 0.00] 193 | --nmoran NMORAN Moran Population Size, if Moran objective [default: 4] 194 | --features FEATURES Number of ANN features [default: 17] 195 | --hidden HIDDEN Number of hidden nodes [default: 10] 196 | --mu_distance DISTANCE Delta max for weights updates [default: 5] 197 | ``` 198 | 199 | ### Finite State Machines 200 | 201 | ```bash 202 | $ python fsm_evolve.py -h 203 | Finite State Machine Evolver 204 | 205 | Usage: 206 | fsm_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION] 207 | [--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSORS] 208 | [--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS] 209 | [--turns TURNS] [--noise NOISE] [--nmoran NMORAN] 210 | [--states NUM_STATES] 211 | 212 | Options: 213 | -h --help Show help 214 | --generations GENERATIONS Generations to run the EA [default: 500] 215 | --population POPULATION Population size [default: 40] 216 | --mu MUTATION_RATE Mutation rate [default: 0.1] 217 | --bottleneck BOTTLENECK Number of individuals to keep from each generation [default: 10] 218 | --processes PROCESSES Number of processes to use [default: 1] 219 | --output OUTPUT_FILE File to write data to [default: fsm_tables.csv] 220 | --objective OBJECTIVE Objective function [default: score] 221 | --repetitions REPETITIONS Repetitions in objective [default: 100] 222 | --turns TURNS Turns in each match [default: 200] 223 | --noise NOISE Match noise [default: 0.00] 224 | --nmoran NMORAN Moran Population Size, if Moran objective [default: 4] 225 | --states NUM_STATES Number of FSM states [default: 8] 226 | ``` 227 | 228 | 229 | ### Hidden Markov Model 230 | 231 | ```bash 232 | $ python hmm_evolve.py -h 233 | Hidden Markov Model Evolver 234 | 235 | Usage: 236 | fsm_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION] 237 | [--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSORS] 238 | [--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS] 239 | [--turns TURNS] [--noise NOISE] [--nmoran NMORAN] 240 | [--states NUM_STATES] 241 | 242 | Options: 243 | -h --help Show help 244 | --generations GENERATIONS Generations to run the EA [default: 500] 245 | --population POPULATION Population size [default: 40] 246 | --mu MUTATION_RATE Mutation rate [default: 0.1] 247 | --bottleneck BOTTLENECK Number of individuals to keep from each generation [default: 10] 248 | --processes PROCESSES Number of processes to use [default: 1] 249 | --output OUTPUT_FILE File to write data to [default: fsm_tables.csv] 250 | --objective OBJECTIVE Objective function [default: score] 251 | --repetitions REPETITIONS Repetitions in objective [default: 100] 252 | --turns TURNS Turns in each match [default: 200] 253 | --noise NOISE Match noise [default: 0.00] 254 | --nmoran NMORAN Moran Population Size, if Moran objective [default: 4] 255 | --states NUM_STATES Number of FSM states [default: 5] 256 | ``` 257 | 258 | ## Open questions 259 | 260 | * What's the best table for n1, n2, m for LookerUp and PSOGambler? What's the 261 | smallest value of the parameters that gives good results? 262 | * Similarly what's the optimal number of states for a finite state machine 263 | strategy? 264 | * What's the best table against parameterized strategies? For example, if the 265 | opponents are `[RandomPlayer(x) for x in np.arange(0, 1, 0.01)], what lookup 266 | table is best? Is it much different from the generic table? 267 | * Are there other features that would improve the performance of EvolvedANN? 268 | 269 | -------------------------------------------------------------------------------- /tests/integration/test_fsm.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import tempfile 3 | import csv 4 | 5 | import axelrod as axl 6 | import axelrod_dojo as dojo 7 | 8 | C, D = axl.Action.C, axl.Action.D 9 | 10 | 11 | player_class=axl.EvolvableFSMPlayer 12 | 13 | 14 | class TestFSMPopulation(unittest.TestCase): 15 | temporary_file = tempfile.NamedTemporaryFile() 16 | 17 | def test_score(self): 18 | name = "score" 19 | turns = 10 20 | noise = 0 21 | repetitions = 5 22 | num_states = 2 23 | opponents = [s() for s in axl.demo_strategies] 24 | size = 10 25 | 26 | objective = dojo.prepare_objective(name=name, 27 | turns=turns, 28 | noise=noise, 29 | repetitions=repetitions) 30 | 31 | population = dojo.Population(player_class=player_class, 32 | params_kwargs={"num_states": num_states}, 33 | size=size, 34 | objective=objective, 35 | output_filename=self.temporary_file.name, 36 | opponents=opponents, 37 | bottleneck=2, 38 | mutation_probability = .01, 39 | processes=1) 40 | 41 | generations = 4 42 | axl.seed(0) 43 | population.run(generations, print_output=False) 44 | self.assertEqual(population.generation, 4) 45 | 46 | # Manually read from tempo file to find best strategy 47 | best_score, best_params = 0, None 48 | with open(self.temporary_file.name, "r") as f: 49 | reader = csv.reader(f) 50 | for row in reader: 51 | _, mean_score, sd_score, max_score, arg_max = row 52 | if float(max_score) > best_score: 53 | best_score = float(max_score) 54 | best_params = arg_max 55 | 56 | # Test the load params function 57 | for num in range(1, 4 + 1): 58 | best = dojo.load_params(player_class=player_class, 59 | filename=self.temporary_file.name, 60 | num=num) 61 | self.assertEqual(len(best), num) 62 | 63 | # Test that can use these loaded params in a new algorithm instance 64 | population = dojo.Population(player_class=player_class, 65 | params_kwargs={"num_states": num_states}, 66 | size=size, 67 | objective=objective, 68 | output_filename=self.temporary_file.name, 69 | opponents=opponents, 70 | population=best, 71 | bottleneck=2, 72 | mutation_probability = .01, 73 | processes=1) 74 | generations = 4 75 | axl.seed(0) 76 | population.run(generations, print_output=False) 77 | self.assertEqual(population.generation, 4) 78 | 79 | def test_score_with_weights(self): 80 | name = "score" 81 | turns = 10 82 | noise = 0 83 | repetitions = 5 84 | num_states = 2 85 | opponents = [s() for s in axl.demo_strategies] 86 | size = 10 87 | 88 | objective = dojo.prepare_objective(name=name, 89 | turns=turns, 90 | noise=noise, 91 | repetitions=repetitions) 92 | 93 | population = dojo.Population(player_class=player_class, 94 | params_kwargs={"num_states": num_states}, 95 | size=size, 96 | objective=objective, 97 | output_filename=self.temporary_file.name, 98 | opponents=opponents, 99 | weights=[5, 1, 1, 1, 1], 100 | bottleneck=2, 101 | mutation_probability = .01, 102 | processes=1) 103 | 104 | generations = 4 105 | axl.seed(0) 106 | population.run(generations, print_output=False) 107 | self.assertEqual(population.generation, 4) 108 | 109 | # Manually read from tempo file to find best strategy 110 | best_score, best_params = 0, None 111 | with open(self.temporary_file.name, "r") as f: 112 | reader = csv.reader(f) 113 | for row in reader: 114 | _, mean_score, sd_score, max_score, arg_max = row 115 | if float(max_score) > best_score: 116 | best_score = float(max_score) 117 | best_params = arg_max 118 | 119 | # Test the load params function 120 | for num in range(1, 4 + 1): 121 | best = dojo.load_params(player_class=player_class, 122 | filename=self.temporary_file.name, 123 | num=num) 124 | self.assertEqual(len(best), num) 125 | self.assertEqual(player_class.serialize_parameters(best[0]), best_params) 126 | 127 | def test_score_with_sample_count(self): 128 | name = "score" 129 | turns = 10 130 | noise = 0 131 | repetitions = 5 132 | num_states = 2 133 | opponents = [s() for s in axl.demo_strategies] 134 | size = 10 135 | 136 | objective = dojo.prepare_objective(name=name, 137 | turns=turns, 138 | noise=noise, 139 | repetitions=repetitions) 140 | 141 | population = dojo.Population(player_class=player_class, 142 | params_kwargs={"num_states": num_states}, 143 | size=size, 144 | objective=objective, 145 | output_filename=self.temporary_file.name, 146 | opponents=opponents, 147 | sample_count=2, # Randomly sample 2 opponents at each step 148 | bottleneck=2, 149 | mutation_probability = .01, 150 | processes=1) 151 | 152 | generations = 4 153 | axl.seed(0) 154 | population.run(generations, print_output=False) 155 | self.assertEqual(population.generation, 4) 156 | 157 | # Manually read from tempo file to find best strategy 158 | best_score, best_params = 0, None 159 | with open(self.temporary_file.name, "r") as f: 160 | reader = csv.reader(f) 161 | for row in reader: 162 | _, mean_score, sd_score, max_score, arg_max = row 163 | if float(max_score) > best_score: 164 | best_score = float(max_score) 165 | best_params = arg_max 166 | 167 | # Test the load params function 168 | for num in range(1, 4 + 1): 169 | best = dojo.load_params(player_class=player_class, 170 | filename=self.temporary_file.name, 171 | num=num) 172 | self.assertEqual(len(best), num) 173 | 174 | self.assertEqual(len(best), num) 175 | self.assertEqual(player_class.serialize_parameters(best[0]), best_params) 176 | 177 | def test_score_with_sample_count_and_weights(self): 178 | name = "score" 179 | turns = 10 180 | noise = 0 181 | repetitions = 5 182 | num_states = 2 183 | opponents = [s() for s in axl.demo_strategies] 184 | size = 10 185 | 186 | objective = dojo.prepare_objective(name=name, 187 | turns=turns, 188 | noise=noise, 189 | repetitions=repetitions) 190 | 191 | population = dojo.Population(player_class=player_class, 192 | params_kwargs={"num_states": num_states}, 193 | size=size, 194 | objective=objective, 195 | output_filename=self.temporary_file.name, 196 | opponents=opponents, 197 | sample_count=2, # Randomly sample 2 opponents at each step 198 | weights=[5, 1, 1, 1, 1], 199 | bottleneck=2, 200 | mutation_probability = .01, 201 | processes=1) 202 | 203 | generations = 4 204 | axl.seed(0) 205 | population.run(generations, print_output=False) 206 | self.assertEqual(population.generation, 4) 207 | 208 | # Manually read from tempo file to find best strategy 209 | best_score, best_params = 0, None 210 | with open(self.temporary_file.name, "r") as f: 211 | reader = csv.reader(f) 212 | for row in reader: 213 | _, mean_score, sd_score, max_score, arg_max = row 214 | if float(max_score) > best_score: 215 | best_score = float(max_score) 216 | best_params = arg_max 217 | 218 | # Test the load params function 219 | for num in range(1, 4 + 1): 220 | best = dojo.load_params(player_class=player_class, 221 | filename=self.temporary_file.name, 222 | num=num) 223 | self.assertEqual(len(best), num) 224 | self.assertEqual(player_class.serialize_parameters(best[0]), best_params) 225 | 226 | def test_score_with_particular_players(self): 227 | """ 228 | These are players that are known to be difficult to pickle 229 | """ 230 | name = "score" 231 | turns = 10 232 | noise = 0 233 | repetitions = 5 234 | num_states = 2 235 | opponents = [axl.ThueMorse(), 236 | axl.MetaHunter(), 237 | axl.BackStabber(), 238 | axl.Alexei()] 239 | size = 10 240 | 241 | objective = dojo.prepare_objective(name=name, 242 | turns=turns, 243 | noise=noise, 244 | repetitions=repetitions) 245 | 246 | population = dojo.Population(player_class=player_class, 247 | params_kwargs={"num_states": num_states}, 248 | size=size, 249 | objective=objective, 250 | output_filename=self.temporary_file.name, 251 | opponents=opponents, 252 | bottleneck=2, 253 | mutation_probability = .01, 254 | processes=0) 255 | 256 | generations = 4 257 | axl.seed(0) 258 | population.run(generations, print_output=False) 259 | self.assertEqual(population.generation, 4) 260 | 261 | def test_population_init_with_given_rate(self): 262 | name = "score" 263 | turns = 10 264 | noise = 0 265 | repetitions = 5 266 | num_states = 2 267 | opponents = [s() for s in axl.demo_strategies] 268 | size = 10 269 | 270 | objective = dojo.prepare_objective(name=name, 271 | turns=turns, 272 | noise=noise, 273 | repetitions=repetitions) 274 | 275 | population = dojo.Population(player_class=player_class, 276 | params_kwargs={"num_states": num_states, 277 | "mutation_probability": .5}, 278 | size=size, 279 | objective=objective, 280 | output_filename=self.temporary_file.name, 281 | opponents=opponents, 282 | bottleneck=2, 283 | mutation_probability = .01, 284 | processes=1) 285 | 286 | for p in population.population: 287 | self.assertEqual(p.mutation_probability, .5) 288 | generations = 1 289 | axl.seed(0) 290 | population.run(generations, print_output=False) 291 | self.assertEqual(population.generation, 1) 292 | -------------------------------------------------------------------------------- /tests/integration/test_hmm.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import tempfile 3 | import csv 4 | 5 | import axelrod as axl 6 | import axelrod_dojo as dojo 7 | 8 | C, D = axl.Action.C, axl.Action.D 9 | 10 | 11 | class TestHMM(unittest.TestCase): 12 | temporary_file = tempfile.NamedTemporaryFile() 13 | 14 | def test_score(self): 15 | name = "score" 16 | turns = 10 17 | noise = 0 18 | repetitions = 5 19 | num_states = 2 20 | opponents = [s() for s in axl.demo_strategies] 21 | size = 10 22 | 23 | objective = dojo.prepare_objective(name=name, 24 | turns=turns, 25 | noise=noise, 26 | repetitions=repetitions) 27 | 28 | population = dojo.Population(player_class=axl.EvolvableHMMPlayer, 29 | params_kwargs={"num_states": num_states}, 30 | size=size, 31 | objective=objective, 32 | output_filename=self.temporary_file.name, 33 | opponents=opponents, 34 | bottleneck=2, 35 | mutation_probability=.01, 36 | processes=1) 37 | 38 | generations = 4 39 | axl.seed(0) 40 | population.run(generations, print_output=False) 41 | self.assertEqual(population.generation, 4) 42 | 43 | # Manually read from temp file to find best strategy 44 | best_score, best_params = 0, None 45 | with open(self.temporary_file.name, "r") as f: 46 | reader = csv.reader(f) 47 | for row in reader: 48 | _, mean_score, sd_score, max_score, arg_max = row 49 | if float(max_score) > best_score: 50 | best_score = float(max_score) 51 | best_params = arg_max 52 | 53 | # Test the load params function 54 | for num in range(1, 4 + 1): 55 | best = dojo.load_params(player_class=axl.EvolvableHMMPlayer, 56 | filename=self.temporary_file.name, 57 | num=num) 58 | self.assertEqual(len(best), num) 59 | 60 | # Test that can use these loaded params in a new algorithm instance 61 | population = dojo.Population(player_class=axl.EvolvableHMMPlayer, 62 | params_kwargs={"num_states": num_states}, 63 | size=size, 64 | objective=objective, 65 | output_filename=self.temporary_file.name, 66 | opponents=opponents, 67 | population=best, 68 | bottleneck=2, 69 | mutation_probability = .01, 70 | processes=1) 71 | generations = 4 72 | axl.seed(0) 73 | population.run(generations, print_output=False) 74 | self.assertEqual(population.generation, 4) 75 | 76 | def test_score_with_weights(self): 77 | name = "score" 78 | turns = 5 79 | noise = 0 80 | repetitions = 5 81 | num_states = 3 82 | opponents = [s() for s in axl.demo_strategies] 83 | size = 10 84 | 85 | objective = dojo.prepare_objective(name=name, 86 | turns=turns, 87 | noise=noise, 88 | repetitions=repetitions) 89 | 90 | population = dojo.Population(player_class=axl.EvolvableHMMPlayer, 91 | params_kwargs={"num_states": num_states}, 92 | size=size, 93 | objective=objective, 94 | output_filename=self.temporary_file.name, 95 | opponents=opponents, 96 | weights=[5, 1, 1, 1, 1], 97 | bottleneck=2, 98 | mutation_probability = .01, 99 | processes=1) 100 | 101 | generations = 4 102 | axl.seed(0) 103 | population.run(generations, print_output=False) 104 | self.assertEqual(population.generation, 4) 105 | 106 | # Manually read from temp file to find best strategy 107 | best_score, best_params = 0, None 108 | with open(self.temporary_file.name, "r") as f: 109 | reader = csv.reader(f) 110 | for row in reader: 111 | _, mean_score, sd_score, max_score, arg_max = row 112 | if float(max_score) > best_score: 113 | best_score = float(max_score) 114 | best_params = arg_max 115 | 116 | # Test the load params function 117 | for num in range(1, 4 + 1): 118 | best = dojo.load_params(player_class=axl.EvolvableHMMPlayer, 119 | filename=self.temporary_file.name, 120 | num=num) 121 | self.assertEqual(len(best), num) 122 | self.assertEqual(best[0].serialize_parameters(), best_params) 123 | 124 | def test_score_with_sample_count(self): 125 | name = "score" 126 | turns = 10 127 | noise = 0 128 | repetitions = 5 129 | num_states = 2 130 | opponents = [s() for s in axl.demo_strategies] 131 | size = 10 132 | 133 | objective = dojo.prepare_objective(name=name, 134 | turns=turns, 135 | noise=noise, 136 | repetitions=repetitions) 137 | 138 | population = dojo.Population(player_class=axl.EvolvableHMMPlayer, 139 | params_kwargs={"num_states": num_states}, 140 | size=size, 141 | objective=objective, 142 | output_filename=self.temporary_file.name, 143 | opponents=opponents, 144 | sample_count=2, # Randomly sample 2 opponents at each step 145 | bottleneck=2, 146 | mutation_probability = .01, 147 | processes=1) 148 | 149 | generations = 4 150 | axl.seed(0) 151 | population.run(generations, print_output=False) 152 | self.assertEqual(population.generation, 4) 153 | 154 | # Manually read from temp file to find best strategy 155 | best_score, best_params = 0, None 156 | with open(self.temporary_file.name, "r") as f: 157 | reader = csv.reader(f) 158 | for row in reader: 159 | _, mean_score, sd_score, max_score, arg_max = row 160 | if float(max_score) > best_score: 161 | best_score = float(max_score) 162 | best_params = arg_max 163 | 164 | # Test the load params function 165 | for num in range(1, 4 + 1): 166 | best = dojo.load_params(player_class=axl.EvolvableHMMPlayer, 167 | filename=self.temporary_file.name, 168 | num=num) 169 | self.assertEqual(len(best), num) 170 | self.assertEqual(best[0].serialize_parameters(), best_params) 171 | 172 | def test_score_with_sample_count_and_weights(self): 173 | name = "score" 174 | turns = 10 175 | noise = 0 176 | repetitions = 5 177 | num_states = 2 178 | opponents = [s() for s in axl.demo_strategies] 179 | size = 10 180 | 181 | objective = dojo.prepare_objective(name=name, 182 | turns=turns, 183 | noise=noise, 184 | repetitions=repetitions) 185 | 186 | population = dojo.Population(player_class=axl.EvolvableHMMPlayer, 187 | params_kwargs={"num_states": num_states}, 188 | size=size, 189 | objective=objective, 190 | output_filename=self.temporary_file.name, 191 | opponents=opponents, 192 | sample_count=2, # Randomly sample 2 opponents at each step 193 | weights=[5, 1, 1, 1, 1], 194 | bottleneck=2, 195 | mutation_probability = .01, 196 | processes=1) 197 | 198 | generations = 4 199 | axl.seed(0) 200 | population.run(generations, print_output=False) 201 | self.assertEqual(population.generation, 4) 202 | 203 | # Manually read from tempo file to find best strategy 204 | best_score, best_params = 0, None 205 | with open(self.temporary_file.name, "r") as f: 206 | reader = csv.reader(f) 207 | for row in reader: 208 | _, mean_score, sd_score, max_score, arg_max = row 209 | if float(max_score) > best_score: 210 | best_score = float(max_score) 211 | best_params = arg_max 212 | 213 | # Test the load params function 214 | for num in range(1, 4 + 1): 215 | best = dojo.load_params(player_class=axl.EvolvableHMMPlayer, 216 | filename=self.temporary_file.name, 217 | num=num) 218 | self.assertEqual(len(best), num) 219 | self.assertEqual(best[0].serialize_parameters(), best_params) 220 | 221 | def test_score_with_particular_players(self): 222 | name = "score" 223 | turns = 10 224 | noise = 0 225 | repetitions = 5 226 | num_states = 2 227 | opponents = [s() for s in axl.basic_strategies] 228 | size = 10 229 | 230 | objective = dojo.prepare_objective(name=name, 231 | turns=turns, 232 | noise=noise, 233 | repetitions=repetitions) 234 | 235 | population = dojo.Population(player_class=axl.EvolvableHMMPlayer, 236 | params_kwargs={"num_states": num_states}, 237 | size=size, 238 | objective=objective, 239 | output_filename=self.temporary_file.name, 240 | opponents=opponents, 241 | bottleneck=2, 242 | mutation_probability = .01, 243 | processes=0) 244 | 245 | generations = 4 246 | axl.seed(0) 247 | population.run(generations, print_output=False) 248 | self.assertEqual(population.generation, 4) 249 | 250 | def test_population_init_with_given_rate(self): 251 | name = "score" 252 | turns = 10 253 | noise = 0 254 | repetitions = 5 255 | num_states = 2 256 | opponents = [s() for s in axl.demo_strategies] 257 | size = 10 258 | 259 | objective = dojo.prepare_objective(name=name, 260 | turns=turns, 261 | noise=noise, 262 | repetitions=repetitions) 263 | 264 | population = dojo.Population(player_class=axl.EvolvableHMMPlayer, 265 | params_kwargs={"num_states": num_states, 266 | "mutation_probability": .5}, 267 | size=size, 268 | objective=objective, 269 | output_filename=self.temporary_file.name, 270 | opponents=opponents, 271 | bottleneck=2, 272 | mutation_probability = .01, 273 | processes=1) 274 | 275 | for p in population.population: 276 | self.assertEqual(p.mutation_probability, .5) 277 | generations = 1 278 | axl.seed(0) 279 | population.run(generations, print_output=False) 280 | self.assertEqual(population.generation, 1) 281 | 282 | def test_score_pso(self): 283 | name = "score" 284 | turns = 5 285 | noise = 0 286 | repetitions = 2 287 | num_states = 4 288 | opponents = [s() for s in axl.demo_strategies] 289 | size = 30 290 | generations = 5 291 | 292 | objective = dojo.prepare_objective(name=name, 293 | turns=turns, 294 | noise=noise, 295 | repetitions=repetitions) 296 | 297 | pso = dojo.PSO(axl.EvolvableHMMPlayer, 298 | params_kwargs={"num_states": num_states}, 299 | objective=objective, 300 | opponents=opponents, 301 | population=size, 302 | generations=generations) 303 | 304 | axl.seed(0) 305 | xopt, fopt = pso.swarm() 306 | 307 | self.assertTrue(len(xopt) == 2 * num_states ** 2 + num_states + 1) 308 | 309 | # You can put the optimal vector back into a HMM. 310 | simple_hmm_opt = axl.EvolvableHMMPlayer(num_states=num_states) 311 | simple_hmm_opt.receive_vector(xopt) 312 | simple_player = simple_hmm_opt 313 | self.assertTrue(simple_player.hmm.is_well_formed()) # This should get asserted in initialization anyway 314 | print(xopt) # As a vector still 315 | print(simple_hmm_opt) # As a HMM 316 | 317 | def test_pso_to_ea(self): 318 | name = "score" 319 | turns = 10 320 | noise = 0 321 | repetitions = 2 322 | num_states = 3 323 | opponents = [s() for s in axl.demo_strategies] 324 | size = 30 325 | generations = 3 326 | 327 | objective = dojo.prepare_objective(name=name, 328 | turns=turns, 329 | noise=noise, 330 | repetitions=repetitions) 331 | 332 | winners = [] 333 | 334 | for _ in range(5): 335 | 336 | axl.seed(_) 337 | 338 | pso = dojo.PSO(axl.EvolvableHMMPlayer, 339 | params_kwargs={"num_states": num_states}, 340 | objective=objective, 341 | opponents=opponents, 342 | population=size, 343 | generations=generations) 344 | 345 | xopt, fopt = pso.swarm() 346 | 347 | # You can put the optimal vector back into a HMM. 348 | hmm_opt = axl.EvolvableHMMPlayer(num_states=num_states) 349 | hmm_opt.receive_vector(xopt) 350 | 351 | winners.append(hmm_opt) 352 | 353 | # Put the winners of the PSO into an EA. 354 | 355 | population = dojo.Population(player_class=axl.EvolvableHMMPlayer, 356 | params_kwargs={"num_states": num_states}, 357 | size=size, 358 | objective=objective, 359 | output_filename=self.temporary_file.name, 360 | opponents=opponents, 361 | population=winners, 362 | bottleneck=2, 363 | mutation_probability = .01, 364 | processes=1) 365 | 366 | axl.seed(0) 367 | population.run(generations, print_output=False) 368 | 369 | # Block resource (?) 370 | with open(self.temporary_file.name, "w") as f: 371 | pass 372 | 373 | scores = population.score_all() 374 | record, record_holder = 0, -1 375 | for i, s in enumerate(scores): 376 | if s >= record: 377 | record = s 378 | record_holder = i 379 | xopt, fopt = population.population[record_holder], record 380 | 381 | print(xopt) 382 | --------------------------------------------------------------------------------