├── .gitignore
├── .gitpod.yml
├── LICENSE
├── Makefile
├── README.md
├── azure-pipelines.yml
├── doc
├── _static
│ ├── css
│ │ └── custom.css
│ ├── evol.png
│ ├── population-1.png
│ ├── quickstart-step-1.png
│ ├── quickstart-step-2.png
│ ├── quickstart-step-3.png
│ ├── quickstart-step-4.png
│ ├── quickstart-step-5.png
│ ├── quickstart-step-6.png
│ ├── quickstart-step-7.png
│ └── quickstart.png
├── api
│ ├── evol.helpers.combiners.rst
│ ├── evol.helpers.mutators.rst
│ ├── evol.helpers.rst
│ ├── evol.problems.functions.rst
│ ├── evol.problems.routing.rst
│ ├── evol.problems.rst
│ ├── evol.rst
│ └── modules.rst
├── conf.py
├── development.rst
├── index.rst
├── population.rst
├── problems.rst
└── quickstart.rst
├── evol
├── __init__.py
├── conditions.py
├── evolution.py
├── exceptions.py
├── helpers
│ ├── __init__.py
│ ├── combiners
│ │ ├── __init__.py
│ │ ├── permutation.py
│ │ └── utils.py
│ ├── groups.py
│ ├── mutators
│ │ ├── __init__.py
│ │ └── permutation.py
│ ├── pickers.py
│ └── utils.py
├── individual.py
├── logger.py
├── population.py
├── problems
│ ├── __init__.py
│ ├── functions
│ │ ├── __init__.py
│ │ └── variableinput.py
│ ├── problem.py
│ └── routing
│ │ ├── __init__.py
│ │ ├── coordinates.py
│ │ ├── magicsanta.py
│ │ └── tsp.py
├── serialization.py
├── step.py
└── utils.py
├── examples
├── number_of_parents.py
├── population_demo.py
├── rock_paper_scissors.py
├── simple_callback.py
├── simple_logging.py
├── simple_nonlinear.py
├── travelling_salesman.py
└── very_basic_tsp.py
├── setup.cfg
├── setup.py
├── test_local.sh
└── tests
├── conftest.py
├── helpers
├── combiners
│ └── test_permutation_combiners.py
├── mutators
│ └── test_permutation_mutators.py
├── test_groups.py
└── test_helpers_utils.py
├── problems
├── test_functions.py
├── test_santa.py
└── test_tsp.py
├── test_callback.py
├── test_conditions.py
├── test_evolution.py
├── test_examples.py
├── test_individual.py
├── test_logging.py
├── test_parallel_population.py
├── test_population.py
├── test_serialization.py
└── test_utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Python template
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | env/
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *,cover
49 | .hypothesis/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # dotenv
85 | .env
86 |
87 | # virtualenv
88 | .venv
89 | venv/
90 | ENV/
91 | evol-env/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 |
96 | # Rope project settings
97 | .ropeproject
98 |
99 | # VSCode
100 | .vscode/
101 |
102 | # PyCharm
103 | .idea/
104 |
105 | # Mac
106 | .DS_Store
107 |
108 | # Pytest
109 | .pytest_cache/
110 |
111 | # documentation build folder
112 | docs
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | tasks:
2 | - init: pyenv local 3.7.2 && pip install -e ".[dev]"
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017-2020 GoDataDriven
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: docs
2 |
3 | flake:
4 | python setup.py flake8
5 |
6 | install:
7 | pip install -e .
8 |
9 | develop:
10 | pip install -e ".[dev]"
11 | python setup.py develop
12 |
13 | test:
14 | python setup.py test
15 |
16 | check: test flake
17 |
18 | docs:
19 | sphinx-apidoc -f -o doc/api evol
20 | sphinx-build doc docs
21 |
22 | clean:
23 | rm -rf .cache
24 | rm -rf .eggs
25 | rm -rf .pytest_cache
26 | rm -rf build
27 | rm -rf dist
28 | rm -rf evol.egg-info
29 | rm -rf .ipynb_checkpoints
30 |
31 | push:
32 | rm -rf dist
33 | python setup.py sdist
34 | python setup.py bdist_wheel --universal
35 | twine upload dist/*
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://evol.readthedocs.io/en/latest/?badge=latest)[](https://pepy.tech/project/evol)
2 | [](https://dev.azure.com/godatadriven/evol/_build/latest?definitionId=9&branchName=master) [](https://evol.readthedocs.io/en/latest/?badge=latest)[](https://pepy.tech/project/evol)
3 |
4 |
5 | 
6 |
7 | `Evol` is clear dsl for composable evolutionary algorithms that optimised for joy.
8 |
9 | ## Installation
10 |
11 | We currently support python3.6 and python3.7 and you can install it via pip.
12 |
13 | ```
14 | pip install evol
15 | ```
16 |
17 | ## Documentation
18 |
19 | For more details you can read the [docs](https://evol.readthedocs.io/en/latest/) but we advice everyone to get start by first checking out the examples in the `/examples` directory. These stand alone examples should show the spirit of usage better than the docs.
20 |
21 | ## The Gist
22 |
23 | The main idea is that you should be able to define a complex algorithm
24 | in a composable way. To explain what we mean by this: let's consider
25 | two evolutionary algorithms for travelling salesman problems.
26 |
27 | The first approach takes a collections of solutions and applies:
28 |
29 | 1. a survival where only the top 50% solutions survive
30 | 2. the population reproduces using a crossover of genes
31 | 3. certain members mutate
32 | 4. repeat this, maybe 1000 times or more!
33 |
34 |
35 |
36 | We can also think of another approach:
37 |
38 | 1. pick the best solution of the population
39 | 2. make random changes to this parent and generate new solutions
40 | 3. repeat this, maybe 1000 times or more!
41 |
42 |
43 |
44 | One could even combine the two algorithms into a new one:
45 |
46 | 1. run algorithm 1 50 times
47 | 2. run algorithm 2 10 times
48 | 3. repeat this, maybe 1000 times or more!
49 |
50 |
51 |
52 | You might notice that many parts of these algorithms are similar and it
53 | is the goal of this library is to automate these parts. We hope to
54 | provide an API that is fun to use and easy to tweak your heuristics in.
55 |
56 | A working example of something silimar to what is depicted above is shown below. You can also find this code as an example in the `/examples/simple_nonlinear.py`.
57 |
58 | ```python
59 | import random
60 | from evol import Population, Evolution
61 |
62 | random.seed(42)
63 |
64 | def random_start():
65 | """
66 | This function generates a random (x,y) coordinate
67 | """
68 | return (random.random() - 0.5) * 20, (random.random() - 0.5) * 20
69 |
70 | def func_to_optimise(xy):
71 | """
72 | This is the function we want to optimise (maximize)
73 | """
74 | x, y = xy
75 | return -(1-x)**2 - 2*(2-x**2)**2
76 |
77 | def pick_random_parents(pop):
78 | """
79 | This is how we are going to select parents from the population
80 | """
81 | mom = random.choice(pop)
82 | dad = random.choice(pop)
83 | return mom, dad
84 |
85 | def make_child(mom, dad):
86 | """
87 | This function describes how two candidates combine into a new candidate
88 | Note that the output is a tuple, just like the output of `random_start`
89 | We leave it to the developer to ensure that chromosomes are of the same type
90 | """
91 | child_x = (mom[0] + dad[0])/2
92 | child_y = (mom[1] + dad[1])/2
93 | return child_x, child_y
94 |
95 | def add_noise(chromosome, sigma):
96 | """
97 | This is a function that will add some noise to the chromosome.
98 | """
99 | new_x = chromosome[0] + (random.random()-0.5) * sigma
100 | new_y = chromosome[1] + (random.random()-0.5) * sigma
101 | return new_x, new_y
102 |
103 | # We start by defining a population with candidates.
104 | pop = Population(chromosomes=[random_start() for _ in range(200)],
105 | eval_function=func_to_optimise, maximize=True)
106 |
107 | # We define a sequence of steps to change these candidates
108 | evo1 = (Evolution()
109 | .survive(fraction=0.5)
110 | .breed(parent_picker=pick_random_parents, combiner=make_child)
111 | .mutate(func=add_noise, sigma=1))
112 |
113 | # We define another sequence of steps to change these candidates
114 | evo2 = (Evolution()
115 | .survive(n=1)
116 | .breed(parent_picker=pick_random_parents, combiner=make_child)
117 | .mutate(func=add_noise, sigma=0.2))
118 |
119 | # We are combining two evolutions into a third one. You don't have to
120 | # but this approach demonstrates the flexibility of the library.
121 | evo3 = (Evolution()
122 | .repeat(evo1, n=50)
123 | .repeat(evo2, n=10)
124 | .evaluate())
125 |
126 | # In this step we are telling evol to apply the evolutions
127 | # to the population of candidates.
128 | pop = pop.evolve(evo3, n=5)
129 | print(f"the best score found: {max([i.fitness for i in pop])}")
130 | ```
131 |
132 | Getting Started
133 | ---------------------------------------
134 |
135 | The best place to get started is the `/examples` folder on github.
136 | This folder contains self contained examples that work out of the
137 | box.
138 |
139 | ## How does it compare to ...
140 |
141 | - [... deap?](https://github.com/DEAP/deap) We think our library is more composable and pythonic while not removing any functionality. Our library may be a bit slower though.
142 | - [... hyperopt?](http://jaberg.github.io/hyperopt/) Since we force the user to make the actual algorithm we are less black boxy. Hyperopt is meant for hyperparameter tuning for machine learning and has better support for search in scikit learn.
143 | - [... inspyred?](https://pypi.org/project/inspyred/) The library offers a simple way to get started but it seems the project is less actively maintained than ours.
144 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | - master
3 | pr:
4 | - master
5 |
6 | pool:
7 | vmImage: 'ubuntu-latest'
8 |
9 |
10 | stages:
11 | - stage: Test
12 | jobs:
13 | - job: TestJob
14 | strategy:
15 | matrix:
16 | Python36:
17 | python.version: '3.6'
18 | Python37:
19 | python.version: '3.7'
20 | Python38:
21 | python.version: '3.8'
22 | steps:
23 | - task: UsePythonVersion@0
24 | inputs:
25 | versionSpec: '$(python.version)'
26 |
27 | - bash: |
28 | pip install --upgrade pip
29 | pip install -e .[dev]
30 | displayName: 'Install'
31 |
32 | - bash: flake8
33 | displayName: 'Flake'
34 |
35 | - bash: python setup.py test
36 | displayName: 'Tests'
37 |
38 | - bash: |
39 | set -e
40 | python examples/simple_nonlinear.py
41 | python examples/number_of_parents.py --n-parents=2 --workers=1
42 | python examples/number_of_parents.py --n-parents=3 --workers=1
43 | python examples/number_of_parents.py --n-parents=4 --workers=1
44 | python examples/number_of_parents.py --n-parents=2 --workers=2
45 | python examples/number_of_parents.py --n-parents=3 --workers=2
46 | python examples/number_of_parents.py --n-parents=4 --workers=2
47 | python examples/very_basic_tsp.py
48 | python examples/simple_logging.py
49 | python examples/rock_paper_scissors.py
50 |
51 |
52 | - stage: Docs
53 | condition: eq(variables['build.sourceBranch'], 'refs/heads/master')
54 | jobs:
55 | - job: DocsJob
56 | steps:
57 | - bash: |
58 | set -e
59 | pip install --upgrade pip
60 | pip install -e .[docs]
61 | sphinx-apidoc -f -o doc/api evol
62 | sphinx-build doc public
63 |
64 | - task: PublishBuildArtifacts@1
65 | inputs:
66 | pathToPublish: public
67 | artifactName: BuildOutput
68 |
--------------------------------------------------------------------------------
/doc/_static/css/custom.css:
--------------------------------------------------------------------------------
1 | .wy-nav-side{
2 | background-color: #f2f2f2;
3 | color: black;
4 | }
5 |
6 | .wy-side-nav-search{
7 | background-color: #404040;
8 | }
9 |
10 | .wy-nav-content{
11 | background-color: #ffffff;
12 | }
13 |
14 | .wy-nav-content-wrap{
15 | background-color: #ffffff;
16 | }
17 |
18 | a.reference.internal{
19 | color: black;
20 | }
21 |
22 | pre{
23 | background: #eeeeee29;
24 | }
25 |
--------------------------------------------------------------------------------
/doc/_static/evol.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/doc/_static/evol.png
--------------------------------------------------------------------------------
/doc/_static/population-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/doc/_static/population-1.png
--------------------------------------------------------------------------------
/doc/_static/quickstart-step-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/doc/_static/quickstart-step-1.png
--------------------------------------------------------------------------------
/doc/_static/quickstart-step-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/doc/_static/quickstart-step-2.png
--------------------------------------------------------------------------------
/doc/_static/quickstart-step-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/doc/_static/quickstart-step-3.png
--------------------------------------------------------------------------------
/doc/_static/quickstart-step-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/doc/_static/quickstart-step-4.png
--------------------------------------------------------------------------------
/doc/_static/quickstart-step-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/doc/_static/quickstart-step-5.png
--------------------------------------------------------------------------------
/doc/_static/quickstart-step-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/doc/_static/quickstart-step-6.png
--------------------------------------------------------------------------------
/doc/_static/quickstart-step-7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/doc/_static/quickstart-step-7.png
--------------------------------------------------------------------------------
/doc/_static/quickstart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/doc/_static/quickstart.png
--------------------------------------------------------------------------------
/doc/api/evol.helpers.combiners.rst:
--------------------------------------------------------------------------------
1 | evol.helpers.combiners package
2 | ==============================
3 |
4 | Submodules
5 | ----------
6 |
7 | evol.helpers.combiners.generic module
8 | -------------------------------------
9 |
10 | .. automodule:: evol.helpers.combiners.generic
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | evol.helpers.combiners.permutation module
16 | -----------------------------------------
17 |
18 | .. automodule:: evol.helpers.combiners.permutation
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 |
24 | Module contents
25 | ---------------
26 |
27 | .. automodule:: evol.helpers.combiners
28 | :members:
29 | :undoc-members:
30 | :show-inheritance:
31 |
--------------------------------------------------------------------------------
/doc/api/evol.helpers.mutators.rst:
--------------------------------------------------------------------------------
1 | evol.helpers.mutators package
2 | =============================
3 |
4 | Submodules
5 | ----------
6 |
7 | evol.helpers.mutators.permutation module
8 | ----------------------------------------
9 |
10 | .. automodule:: evol.helpers.mutators.permutation
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 |
16 | Module contents
17 | ---------------
18 |
19 | .. automodule:: evol.helpers.mutators
20 | :members:
21 | :undoc-members:
22 | :show-inheritance:
23 |
--------------------------------------------------------------------------------
/doc/api/evol.helpers.rst:
--------------------------------------------------------------------------------
1 | evol.helpers package
2 | ====================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 |
9 | evol.helpers.combiners
10 | evol.helpers.mutators
11 |
12 | Submodules
13 | ----------
14 |
15 | evol.helpers.pickers module
16 | ---------------------------
17 |
18 | .. automodule:: evol.helpers.pickers
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | evol.helpers.utils module
24 | -------------------------
25 |
26 | .. automodule:: evol.helpers.utils
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 |
32 | Module contents
33 | ---------------
34 |
35 | .. automodule:: evol.helpers
36 | :members:
37 | :undoc-members:
38 | :show-inheritance:
39 |
--------------------------------------------------------------------------------
/doc/api/evol.problems.functions.rst:
--------------------------------------------------------------------------------
1 | evol.problems.functions package
2 | ===============================
3 |
4 | Submodules
5 | ----------
6 |
7 | evol.problems.functions.variableinput module
8 | --------------------------------------------
9 |
10 | .. automodule:: evol.problems.functions.variableinput
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 |
16 | Module contents
17 | ---------------
18 |
19 | .. automodule:: evol.problems.functions
20 | :members:
21 | :undoc-members:
22 | :show-inheritance:
23 |
--------------------------------------------------------------------------------
/doc/api/evol.problems.routing.rst:
--------------------------------------------------------------------------------
1 | evol.problems.routing package
2 | =============================
3 |
4 | Submodules
5 | ----------
6 |
7 | evol.problems.routing.coordinates module
8 | ----------------------------------------
9 |
10 | .. automodule:: evol.problems.routing.coordinates
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | evol.problems.routing.magicsanta module
16 | ---------------------------------------
17 |
18 | .. automodule:: evol.problems.routing.magicsanta
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | evol.problems.routing.tsp module
24 | --------------------------------
25 |
26 | .. automodule:: evol.problems.routing.tsp
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 |
32 | Module contents
33 | ---------------
34 |
35 | .. automodule:: evol.problems.routing
36 | :members:
37 | :undoc-members:
38 | :show-inheritance:
39 |
--------------------------------------------------------------------------------
/doc/api/evol.problems.rst:
--------------------------------------------------------------------------------
1 | evol.problems package
2 | =====================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 |
9 | evol.problems.functions
10 | evol.problems.routing
11 |
12 | Submodules
13 | ----------
14 |
15 | evol.problems.problem module
16 | ----------------------------
17 |
18 | .. automodule:: evol.problems.problem
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 |
24 | Module contents
25 | ---------------
26 |
27 | .. automodule:: evol.problems
28 | :members:
29 | :undoc-members:
30 | :show-inheritance:
31 |
--------------------------------------------------------------------------------
/doc/api/evol.rst:
--------------------------------------------------------------------------------
1 | evol package
2 | ============
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 |
9 | evol.helpers
10 | evol.problems
11 |
12 | Submodules
13 | ----------
14 |
15 | evol.evolution module
16 | ---------------------
17 |
18 | .. automodule:: evol.evolution
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | evol.individual module
24 | ----------------------
25 |
26 | .. automodule:: evol.individual
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | evol.logger module
32 | ------------------
33 |
34 | .. automodule:: evol.logger
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 | evol.population module
40 | ----------------------
41 |
42 | .. automodule:: evol.population
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
47 | evol.serialization module
48 | -------------------------
49 |
50 | .. automodule:: evol.serialization
51 | :members:
52 | :undoc-members:
53 | :show-inheritance:
54 |
55 | evol.step module
56 | ----------------
57 |
58 | .. automodule:: evol.step
59 | :members:
60 | :undoc-members:
61 | :show-inheritance:
62 |
63 |
64 | Module contents
65 | ---------------
66 |
67 | .. automodule:: evol
68 | :members:
69 | :undoc-members:
70 | :show-inheritance:
71 |
--------------------------------------------------------------------------------
/doc/api/modules.rst:
--------------------------------------------------------------------------------
1 | evol
2 | ====
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | evol
8 |
--------------------------------------------------------------------------------
/doc/conf.py:
--------------------------------------------------------------------------------
1 | import evol
2 |
3 | # -*- coding: utf-8 -*-
4 | #
5 | # Configuration file for the Sphinx documentation builder.
6 | #
7 | # This file does only contain a selection of the most common options. For a
8 | # full list see the documentation:
9 | # http://www.sphinx-doc.org/en/master/config
10 |
11 | # -- Path setup --------------------------------------------------------------
12 |
13 | # If extensions (or modules to document with autodoc) are in another directory,
14 | # add these directories to sys.path here. If the directory is relative to the
15 | # documentation root, use os.path.abspath to make it absolute, like shown here.
16 | #
17 | # import os
18 | # import sys
19 | # sys.path.insert(0, os.path.abspath('.'))
20 |
21 |
22 | # -- Project information -----------------------------------------------------
23 |
24 | project = 'evol'
25 | copyright = '2019, Vincent D. Warmerdam & Rogier van der Geer'
26 | author = 'Vincent D. Warmerdam & Rogier van der Geer'
27 |
28 | # The short X.Y version
29 | version = ''
30 | # The full version, including alpha/beta/rc tags
31 | release = evol.__version__
32 |
33 |
34 | # -- General configuration ---------------------------------------------------
35 |
36 | # If your documentation needs a minimal Sphinx version, state it here.
37 | #
38 | # needs_sphinx = '1.0'
39 |
40 | # Add any Sphinx extension module names here, as strings. They can be
41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
42 | # ones.
43 | extensions = [
44 | 'sphinx.ext.autodoc',
45 | 'sphinx.ext.mathjax',
46 | 'sphinx.ext.viewcode',
47 | ]
48 |
49 | # Add any paths that contain templates here, relative to this directory.
50 | templates_path = ['_templates']
51 |
52 | # The suffix(es) of doc filenames.
53 | # You can specify multiple suffix as a list of string:
54 | #
55 | # source_suffix = ['.rst', '.md']
56 | source_suffix = '.rst'
57 |
58 | # The master toctree document.
59 | master_doc = 'index'
60 |
61 | # The language for content autogenerated by Sphinx. Refer to documentation
62 | # for a list of supported languages.
63 | #
64 | # This is also used if you do content translation via gettext catalogs.
65 | # Usually you set "language" from the command line for these cases.
66 | language = None
67 |
68 | # List of patterns, relative to doc directory, that match files and
69 | # directories to ignore when looking for doc files.
70 | # This pattern also affects html_static_path and html_extra_path.
71 | exclude_patterns = []
72 |
73 | # The name of the Pygments (syntax highlighting) style to use.
74 | pygments_style = None
75 |
76 |
77 | # -- Options for HTML output -------------------------------------------------
78 |
79 | # The theme to use for HTML and HTML Help pages. See the documentation for
80 | # a list of builtin themes.
81 | #
82 | html_theme = 'sphinx_rtd_theme'
83 |
84 | # Theme options are theme-specific and customize the look and feel of a theme
85 | # further. For a list of options available for each theme, see the
86 | # documentation.
87 | #
88 | # html_theme_options = {}
89 |
90 | # Add any paths that contain custom static files (such as style sheets) here,
91 | # relative to this directory. They are copied after the builtin static files,
92 | # so a file named "default.css" will overwrite the builtin "default.css".
93 | html_static_path = ['_static']
94 |
95 | # Custom sidebar templates, must be a dictionary that maps document names
96 | # to template names.
97 | #
98 | # The default sidebars (for documents that don't match any pattern) are
99 | # defined by theme itself. Builtin themes are using these templates by
100 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
101 | # 'searchbox.html']``.
102 | #
103 | # html_sidebars = {}
104 |
105 |
106 | # -- Options for HTMLHelp output ---------------------------------------------
107 |
108 | # Output file base name for HTML help builder.
109 | htmlhelp_basename = 'evoldoc'
110 |
111 |
112 | # -- Options for LaTeX output ------------------------------------------------
113 |
114 | latex_elements = {
115 | # The paper size ('letterpaper' or 'a4paper').
116 | #
117 | # 'papersize': 'letterpaper',
118 |
119 | # The font size ('10pt', '11pt' or '12pt').
120 | #
121 | # 'pointsize': '10pt',
122 |
123 | # Additional stuff for the LaTeX preamble.
124 | #
125 | # 'preamble': '',
126 |
127 | # Latex figure (float) alignment
128 | #
129 | # 'figure_align': 'htbp',
130 | }
131 |
132 | # Grouping the document tree into LaTeX files. List of tuples
133 | # (doc start file, target name, title,
134 | # author, documentclass [howto, manual, or own class]).
135 | latex_documents = [
136 | (master_doc, 'evol.tex', 'evol Documentation',
137 | 'Vincent D. Warmerdam \\& Rogier van der Geer', 'manual'),
138 | ]
139 |
140 |
141 | # -- Options for manual page output ------------------------------------------
142 |
143 | # One entry per manual page. List of tuples
144 | # (doc start file, name, description, authors, manual section).
145 | man_pages = [
146 | (master_doc, 'evol', 'evol Documentation',
147 | [author], 1)
148 | ]
149 |
150 |
151 | # -- Options for Texinfo output ----------------------------------------------
152 |
153 | # Grouping the document tree into Texinfo files. List of tuples
154 | # (doc start file, target name, title, author,
155 | # dir menu entry, description, category)
156 | texinfo_documents = [
157 | (master_doc, 'evol', 'evol Documentation',
158 | author, 'evol', 'One line description of project.',
159 | 'Miscellaneous'),
160 | ]
161 |
162 |
163 | # -- Options for Epub output -------------------------------------------------
164 |
165 | # Bibliographic Dublin Core info.
166 | epub_title = project
167 |
168 | # The unique identifier of the text. This can be a ISBN number
169 | # or the project homepage.
170 | #
171 | # epub_identifier = ''
172 |
173 | # A unique identification for the text.
174 | #
175 | # epub_uid = ''
176 |
177 | # A list of files that should not be packed into the epub file.
178 | epub_exclude_files = ['search.html']
179 |
180 |
181 | # -- Extension configuration -------------------------------------------------
182 |
183 | def setup(app):
184 | print("Custom part of setup is now running...")
185 | app.add_stylesheet('css/custom.css')
186 | print("Custom part of setup is now complete.")
187 |
--------------------------------------------------------------------------------
/doc/development.rst:
--------------------------------------------------------------------------------
1 | .. image:: https://i.imgur.com/7MHcIq1.png
2 | :align: center
3 |
4 | Development
5 | ===========
6 |
7 | Installing from PyPi
8 | ^^^^^^^^^^^^^^^^^^^^
9 |
10 | We currently support python3.6 and python3.7 and you can install it via pip.
11 |
12 | .. code-block:: bash
13 |
14 | pip install evol
15 |
16 | Developing Locally with Makefile
17 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 |
19 | You can also fork/clone the repository on Github_ to work on it locally. we've
20 | added a `Makefile` to the project that makes it easy to install everything ready
21 | for development.
22 |
23 | .. code-block:: bash
24 |
25 | make develop
26 |
27 | There's some other helpful commands in there. For example, testing can be done via;
28 |
29 | .. code-block:: bash
30 |
31 | make test
32 |
33 | This will pytest and possibly in the future also the docstring tests.
34 |
35 | Generating Documentation
36 | ^^^^^^^^^^^^^^^^^^^^^^^^
37 |
38 | The easiest way to generate documentation is by running:
39 |
40 | .. code-block:: bash
41 |
42 | make docs
43 |
44 | This will populate the `/docs` folder locally. Note that we ignore the
45 | contents of the this folder per git ignore because building the documentation
46 | is something that we outsource to the read-the-docs service.
47 |
48 | .. _Github: https://scikit-learn.org/stable/modules/compose.html
49 |
50 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | .. evol documentation master file, created by
2 | sphinx-quickstart on Thu Apr 4 09:34:54 2019.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | **Evol** is a clear dsl for composable evolutionary algorithms, optimised for joy.
7 |
8 | .. image:: _static/evol.png
9 | :align: center
10 |
11 | .. code-block:: bash
12 |
13 | pip install evol
14 |
15 | The Gist
16 | ********
17 |
18 | The main idea is that you should be able to define a complex algorithm
19 | in a composable way. To explain what we mean by this: let's consider
20 | two evolutionary algorithms for travelling salesman problems.
21 |
22 | The first approach takes a collections of solutions and applies:
23 |
24 | 1. a survival where only the top 50% solutions survive
25 | 2. the population reproduces using a crossover of genes
26 | 3. certain members mutate
27 | 4. repeat this, maybe 1000 times or more!
28 |
29 | .. image:: https://i.imgur.com/is9g07u.png
30 | :align: center
31 |
32 | We can also think of another approach:
33 |
34 | 1. pick the best solution of the population
35 | 2. make random changes to this parent and generate new solutions
36 | 3. repeat this, maybe 1000 times or more!
37 |
38 | .. image:: https://i.imgur.com/JRSWbTd.png
39 | :align: center
40 |
41 | One could even combine the two algorithms into a new one:
42 |
43 | 1. run algorithm 1 50 times
44 | 2. run algorithm 2 10 times
45 | 3. repeat this, maybe 1000 times or more!
46 |
47 | .. image:: https://i.imgur.com/SZTBWX2.png
48 | :align: center
49 |
50 | You might notice that many parts of these algorithms are similar and it
51 | is the goal of this library is to automate these parts. We hope to
52 | provide an API that is fun to use and easy to tweak your heuristics in.
53 |
54 | .. toctree::
55 | :maxdepth: 2
56 | :caption: Contents:
57 |
58 | quickstart
59 | population
60 | problems
61 | development
62 |
63 | Indices and tables
64 | ==================
65 |
66 | * :ref:`genindex`
67 | * :ref:`modindex`
68 | * :ref:`search`
69 |
--------------------------------------------------------------------------------
/doc/population.rst:
--------------------------------------------------------------------------------
1 | Population Guide
2 | ================
3 |
4 | The "Population" object in **evol** is the base container for all your
5 | candidate solutions. Each candidate solution (sometimes referred to
6 | as a chromosome in the literature) lives inside of the population as
7 | an "Individual" and has a fitness score attached as a property.
8 |
9 | .. image:: _static/population-1.png
10 | :align: center
11 |
12 | You do not typically deal with "Individual" objects directly but it
13 | is useful to know that they are data containers that have a chromosome
14 | property as well as a fitness property.
15 |
16 | Creation
17 | ********
18 |
19 | In order to create a population you need an evaluation function and either:
20 |
21 | 1. a collection of candidate solutions
22 | 2. a function that can generate candidate solutions
23 |
24 | Both methods of initialising a population are demonstrated below.
25 |
26 | .. literalinclude:: ../examples/population_demo.py
27 | :lines: 1-25
28 |
29 | Lazy Evaluation
30 | ***************
31 |
32 | If we were to now query the contents of the population object
33 | you can use a for loop to view some of the contents.
34 |
35 | .. code-block:: python
36 |
37 | > [i for i in pop1]
38 | [,
39 | ,
40 | ,
41 | ,
42 | ]
43 | > [i.chromosome for i in pop1]
44 | [ 0.13942679845788375,
45 | -0.47498924477733306,
46 | -0.22497068163088074,
47 | -0.27678926185117725,
48 | 0.2364712141640124]
49 |
50 | You might be slightly suprised by the following result though.
51 |
52 | .. code-block:: python
53 |
54 | > [i.fitness for i in pop1]
55 | [None, None, None, None, None]
56 |
57 | The fitness property seems to not exist. But if we call the "evaluate"
58 | method first then suddenly it does seem to make an appearance.
59 |
60 | .. code-block:: python
61 |
62 | > [i.fitness for i in pop1.evaluate()]
63 | [ 0.2788535969157675,
64 | -0.9499784895546661,
65 | -0.4499413632617615,
66 | -0.5535785237023545,
67 | 0.4729424283280248]
68 |
69 | There is some logic behind this. Typically the evaluation function
70 | can be very expensive to calculate so you might want to consider running
71 | it as late as possible and as few times as possible. The only command
72 | that needs a fitness is the "survive" method. All other methods can apply
73 | transformations to the chromosome without needing to evaluate the fitness.
74 |
75 | More Lazyness
76 | *************
77 |
78 | To demonstrate the effect of this lazyness, let's see the effect
79 | of the fitness of the individuals.
80 |
81 | First, note that after a survive method everything is evaluated.
82 |
83 | .. code-block:: python
84 |
85 | > [i.fitness for i in pop1.survive(n=3)]
86 | [0.4729424283280248, 0.2788535969157675, -0.4499413632617615]
87 |
88 | If we were to add a mutate step afterwards we will see that the
89 | lazyness kicks in again. Only if we add an evaluate step will we
90 | see fitness values again.
91 |
92 | .. code-block:: python
93 |
94 | def add_noise(x):
95 | return 0.1 * (random.random() - 0.5) + x
96 |
97 | > [i.fitness for i in pop1.survive(n=3).mutate(add_noise)]
98 | [None, None, None]
99 | > [i.fitness for i in pop1.survive(n=3).mutate(add_noise).evaluate()]
100 | [0.3564375534260752, 0.30990154209234466, -0.5356458732043454]
101 |
102 | If you want to work with fitness values explicitly it is good to know
103 | about this, otherwise the library will try to be as conservative as
104 | possible when it comes to evaluating the fitness function.
105 |
--------------------------------------------------------------------------------
/doc/problems.rst:
--------------------------------------------------------------------------------
1 | Problem Guide
2 | =============
3 |
4 | Certain problems are general enough, if only for educational
5 | purposes, to include into our API. This guide will demonstrate
6 | some of problems that are included in evol.
7 |
8 | General Idea
9 | ------------
10 |
11 | In general a problem in evol is nothing more than an object
12 | that has `.eval_function()` implemented. This object can
13 | usually be initialised in different ways but the method
14 | must always be implemented.
15 |
16 | Function Problems
17 | -----------------
18 |
19 | There are a few hard functions out there that can be optimised
20 | with heuristics. Our library offers a few objects with this
21 | implementation.
22 |
23 | The following functions are implemented.
24 |
25 | .. code-block:: python
26 |
27 | from evol.problems.functions import Rastrigin, Sphere, Rosenbrock
28 |
29 | Rastrigin(size=1).eval_function([1])
30 | Sphere(size=2).eval_function([2, 1])
31 | Rosenbrock(size=3).eval_function([3, 2, 1])
32 |
33 | You may notice that we pass a size parameter apon initialisation; this
34 | is because these functions can also be defined in higher dimensions.
35 | Feel free to check the wikipedia_ article for more explanation on these functions.
36 |
37 |
38 | Routing Problems
39 | ----------------
40 |
41 | Traveling Salesman Problem
42 | **************************
43 |
44 | It's a classic problem so we've included it here.
45 |
46 | .. code-block:: python
47 |
48 | import random
49 | from evol.problems.routing import TSPProblem, coordinates
50 |
51 | us_cities = coordinates.united_states_capitols
52 | problem = TSPProblem.from_coordinates(coordinates=us_cities)
53 |
54 | order = list(range(len(us_cities)))
55 | for i in range(3):
56 | random.shuffle(order)
57 | print(problem.eval_function(order))
58 |
59 | Note that you can also create an instance of a TSP problem
60 | from a distance matrix instead. Also note that you can get
61 | such a distance matrix from the object.
62 |
63 | .. code:: python
64 |
65 | same_problem = TSPProblem(problem.distance_matrix)
66 | print(same_problem.eval_function(order))
67 |
68 | Magic Santa
69 | ***********
70 |
71 | This problem was inspired by a kaggle_ competition. It involves the logistics
72 | of delivering gifts all around the world from the north pole. The costs of
73 | delivering a gift depend on how tired santa's reindeer get while delivering
74 | a sleigh full of gifts during a trip.
75 |
76 |
77 | It is better explained on the website than here but the goal is to
78 | minimize the weighed reindeer weariness defined below:
79 |
80 | :math:`WRW = \sum\limits_{j=1}^{m} \sum\limits_{i=1}^{n} \Big[ \big( \sum\limits_{k=1}^{n} w_{kj} - \sum\limits_{k=1}^{i} w_{kj} \big) \cdot Dist(Loc_i, Loc_{i-1})`
81 |
82 | In terms of setting up the problem it is very similar to a TSP except that
83 | we now also need to attach the weight of a gift per location.
84 |
85 | .. code:: python
86 |
87 | import random
88 | from evol.problems.routing import MagicSanta, coordinates
89 |
90 | us_cities = coordinates.united_states_capitols
91 | problem = TSPProblem.from_coordinates(coordinates=us_cities)
92 |
93 | MagicSanta(city_coordinates=us_cities,
94 | home_coordinate=(0, 0),
95 | gift_weight=[random.random() for _ in us_cities])
96 |
97 | .. _wikipedia: https://en.wikipedia.org/wiki/Test_functions_for_optimization
98 | .. _kaggle: https://www.kaggle.com/c/santas-stolen-sleigh#evaluation
--------------------------------------------------------------------------------
/doc/quickstart.rst:
--------------------------------------------------------------------------------
1 |
2 | Quick-Start Guide
3 | =================
4 |
5 | The goal is this document is to build the pipeline you see below.
6 |
7 | .. image:: _static/quickstart-step-6.png
8 | :align: center
9 |
10 | This guide will offer a step by step guide on how to use evol
11 | to write custom heuristic solutions to problems. As an example
12 | we will try to optimise the following non-linear function:
13 |
14 | :math:`f(x, y) = -(1-x)^2 - (2 - y^2)^2`
15 |
16 | Step 1: Score
17 | ^^^^^^^^^^^^^
18 |
19 | The first thing we need to do for evol is to describe how
20 | "good" a solution to a problem is. To facilitate this we
21 | can write a simple function.
22 |
23 | .. literalinclude:: ../examples/simple_nonlinear.py
24 | :lines: 15-20
25 |
26 | You'll notice that this function accepts a "solution" to
27 | the problem and it returns a value. In this case the "solution"
28 | is a list that contains two elements. Inside the function we
29 | unpack it but the function that we have needs to accept one
30 | "candidate"-solution and return one score.
31 |
32 | Step 2: Sample
33 | ^^^^^^^^^^^^^^
34 |
35 | Another thing we need is something that can create
36 | random candidates. We want our algorithm to start searching
37 | somewhere and we prefer to start with different candidates
38 | instead of a static set. The function below will generate
39 | such candidates.
40 |
41 | .. literalinclude:: ../examples/simple_nonlinear.py
42 | :lines: 8-12
43 |
44 | Note that one candidate from this function will create
45 | a tuple; one that can be unpacked by the function we've defined
46 | before.
47 |
48 | Step 3: Create
49 | ^^^^^^^^^^^^^^
50 |
51 | With these two functions we can create a population of
52 | candidates. Below we generate a population with 200 random
53 | candidates.
54 |
55 | .. literalinclude:: ../examples/simple_nonlinear.py
56 | :language: python
57 | :lines: 54-55
58 |
59 | This population object is merely a container for candidates.
60 | The next step is to define things that we might want to do
61 | with it.
62 |
63 | If we were to draw where we currently are, it'd be here:
64 |
65 | .. image:: _static/quickstart-step-1.png
66 | :align: center
67 |
68 | Step 4: Survive
69 | ^^^^^^^^^^^^^^^
70 |
71 | Now that we have a population we might add a bit of code that
72 | can remove candidates that are not performing as well. This means
73 | that we add a step to our "pipeline".
74 |
75 | .. image:: _static/quickstart-step-2.png
76 | :align: center
77 |
78 | To facilitate this we merely need to call a method on our population object.
79 |
80 | .. literalinclude:: ../examples/simple_nonlinear.py
81 | :language: python
82 | :lines: 57-58
83 |
84 | Because the population knows what it needs to optimise for
85 | it is easy to halve the population size by simply calling this method.
86 | This method call will return a new population object that has fewer
87 | members. A next step might be to take these remaining candidates and
88 | to use them to create new candidates that are similar.
89 |
90 | Step 5: Breed
91 | ^^^^^^^^^^^^^
92 |
93 | In order to evolve the candidates we need to start generating
94 | new candites again. This adds another step to our pipeline:
95 |
96 | .. image:: _static/quickstart-step-3.png
97 | :align: center
98 |
99 | Note that in this view the highlighted candidates are the new ones
100 | that have been created. The candidates who were already performing
101 | very well are still in the population.
102 |
103 | To generate new candidates we need to do two things:
104 |
105 | 1. we need to determine what parents will be used to create a new individual
106 | 2. we need to determine how these parent candidates create a new one
107 |
108 | Both steps needs to be defined in functions. First, we write
109 | a simple function that will select two random parents from
110 | the population.
111 |
112 | .. literalinclude:: ../examples/simple_nonlinear.py
113 | :language: python
114 | :lines: 23-29
115 |
116 | Next we need a function that can merge the properties of these
117 | two parents such that we create a new candidate.
118 |
119 | .. literalinclude:: ../examples/simple_nonlinear.py
120 | :language: python
121 | :lines: 32-41
122 |
123 | With these two functions we can expand our initial pipeline
124 | and expand it with a breed step.
125 |
126 |
127 | .. literalinclude:: ../examples/simple_nonlinear.py
128 | :language: python
129 | :lines: 60-63
130 |
131 | Step 6: Mutate
132 | ^^^^^^^^^^^^^^
133 |
134 | Typically when searching for a good candidate we might want
135 | to add some entropy in the system. The idea being that a bit
136 | of random search might indeed help us explore areas that we
137 | might not otherwise consider.
138 |
139 | .. image:: _static/quickstart-step-4.png
140 | :align: center
141 |
142 | The idea is to add a bit of noise to every single datapoint.
143 | This ensures that our population of candidates does not converge
144 | too fast towards a single datapoint and that we are able to
145 | explore the search space.
146 |
147 | To faciliate this in our pipeline we first need to create
148 | a function that can take a candidate and apply the noise.
149 |
150 | .. literalinclude:: ../examples/simple_nonlinear.py
151 | :language: python
152 | :lines: 44-50
153 |
154 | Next we need to add this as a step in our pipeline.
155 |
156 | .. literalinclude:: ../examples/simple_nonlinear.py
157 | :language: python
158 | :lines: 65-69
159 |
160 | Step 7: Repeat
161 | ^^^^^^^^^^^^^^
162 |
163 | We're getting really close to where we want to be now but
164 | we still need to discuss how to repeat our steps.
165 |
166 | .. image:: _static/quickstart-step-5.png
167 | :align: center
168 |
169 | One way of getting there is to literally repeat the code
170 | we saw earlier in a for loop.
171 |
172 | .. literalinclude:: ../examples/simple_nonlinear.py
173 | :language: python
174 | :lines: 71-76
175 |
176 | This sort of works, but there is a more elegant method.
177 |
178 | Step 8: Evolve
179 | ^^^^^^^^^^^^^^
180 |
181 | The problem with the previous method is that we don't
182 | just want to repeat but we also want to supply settings
183 | to our evolution steps that might change over time. To
184 | facilitate this our api offers the `Evolution` object.
185 |
186 | .. image:: _static/quickstart-step-6.png
187 | :align: center
188 |
189 | You can see a `Population` as a container for candidates
190 | and can `Evolution` as a container for changes to the
191 | population. You can use the exact same verbs in the method
192 | chain to specify what you'd like to see happen but it allows
193 | you much more fledixbility.
194 |
195 | The code below demonstrates an example of evolution steps.
196 |
197 | .. literalinclude:: ../examples/simple_nonlinear.py
198 | :language: python
199 | :lines: 78-82
200 |
201 | The code below demonstrates a slightly different set of steps.
202 |
203 | .. literalinclude:: ../examples/simple_nonlinear.py
204 | :language: python
205 | :lines: 84-88
206 |
207 | Evolutions are kind of flexible, we can combine these two
208 | evolutions into a third one.
209 |
210 | .. literalinclude:: ../examples/simple_nonlinear.py
211 | :language: python
212 | :lines: 90-96
213 |
214 | Now if you'd like to apply this evolution we've added a method
215 | for that on top of our evolution object.
216 |
217 | .. literalinclude:: ../examples/simple_nonlinear.py
218 | :language: python
219 | :lines: 98-101
220 |
221 | Step 9: Dream
222 | ^^^^^^^^^^^^^^
223 |
224 | These steps together give us an evolution program depicted below.
225 |
226 | .. image:: _static/quickstart-step-7.png
227 | :align: center
228 |
229 | The goal of evol is to make it easy to write heuristic pipelines
230 | that can help search towards a solution. Note that you don't need
231 | to write a genetic algorithm here. You could also implement simulated
232 | annealing in our library just as easily but we want to help you standardise
233 | your code such that testing, monitoring, parallism and checkpoint becomes
234 | more joyful.
235 |
236 | Evol will help you structure your pipeline by giving a language that
237 | tells you *what* is happening but not *how* this is being done. For this
238 | you will need to write functions yourself because our library has no
239 | notion of your specific problem.
240 |
241 | We hope this makes writing heuristic software more fun.
--------------------------------------------------------------------------------
/evol/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 
3 |
4 | `Evol` is clear dsl for composable evolutionary algorithms that optimised for joy.
5 |
6 | Evol is a library that helps you make evolutionary algorithms. The
7 | goal is to have a library that is fun and clear to use, but not fast.
8 |
9 | If you're looking at the library for the first time we recommend
10 | that you first take a look at the examples in the /examples folder
11 | on github. It is usually a better starting point to get started.
12 |
13 | Any details can be discovered on the docs. We hope that this
14 | library makes it fun to write heuristics again.
15 |
16 | The Gist
17 | ---------------------------------------
18 |
19 | The main idea is that you should be able to define a complex algorithm
20 | in a composable way. To explain what we mean by this: let's consider
21 | two evolutionary algorithms for travelling salesman problems.
22 |
23 | The first approach takes a collections of solutions and applies:
24 |
25 | 1. a survival where only the top 50% solutions survive
26 | 2. the population reproduces using a crossover of genes
27 | 3. certain members mutate
28 | 4. repeat this, maybe 1000 times or more!
29 |
30 |
31 |
32 | We can also think of another approach:
33 |
34 | 1. pick the best solution of the population
35 | 2. make random changes to this parent and generate new solutions
36 | 3. repeat this, maybe 1000 times or more!
37 |
38 |
39 |
40 | One could even combine the two algorithms into a new one:
41 |
42 | 1. run algorithm 1 50 times
43 | 2. run algorithm 2 10 times
44 | 3. repeat this, maybe 1000 times or more!
45 |
46 |
47 |
48 | You might notice that many parts of these algorithms are similar and it is
49 | the goal of this library is to automate these parts. In fact, you can
50 | expect the code for these algorithms to look something like this.
51 |
52 | A speudo-example of what is decribed about looks a bit like this:
53 |
54 | import random
55 | from evol import Population, Evolution
56 |
57 | population = Population(init_func=init_func, eval_func=eval_func, size=100)
58 |
59 | def pick_n_parents(population, num_parents):
60 | return [random.choice(population) for i in range(num_parents)]
61 |
62 | def crossover(*parents):
63 | ...
64 |
65 | def random_copy(parent):
66 | ...
67 |
68 | evo1 = (Evolution(name="first_algorithm")
69 | .survive(fraction=0.5)
70 | .breed(parentpicker=pick_n_parents,
71 | combiner=combiner,
72 | num_parents=2, n_max=100)
73 | .mutate(lambda x: add_noise(x, 0.1)))
74 |
75 | evo2 = (Evolution(name="second_algorithm")
76 | .survive(n=1)
77 | .breed(parentpicker=pick_n_parents,
78 | combiner=random_copy,
79 | num_parents=1, n_max=100))
80 |
81 | for i in range(1001):
82 | population.evolve(evo1, n=50).evolve(evo2, n=10)
83 |
84 | Getting Started
85 | ---------------------------------------
86 |
87 | The best place to get started is the `/examples` folder on github.
88 | This folder contains self contained examples that work out of the
89 | box.
90 |
91 | Contributing Guide
92 | ---------------------------------------
93 |
94 | ### Local development
95 |
96 | Python can help you. Don't reinstall all the time, rather use a
97 | virtulenv that has a link to the code.
98 |
99 | python setup.py develop
100 |
101 | When you submit a pull request it will be tested in travis. Once
102 | the build is green the review can start. Please try to keep your
103 | branch up to date to ensure you're not missing any tests. Larger
104 | commits need to be discussed in github before we can accept them.
105 |
106 | ### Generating New Documentation
107 |
108 | Updating documentation is currently a manual step. From the `docs` folder:
109 |
110 | pdoc --html --overwrite evol
111 | cp -rf evol/* .
112 | rm -rf evol
113 |
114 | If you want to confirm that it works you can open the `index.html` file.
115 | """
116 |
117 | from .individual import Individual
118 | from .population import Population, ContestPopulation
119 | from .evolution import Evolution
120 | from .logger import BaseLogger
121 |
122 | __version__ = "0.5.3"
123 |
--------------------------------------------------------------------------------
/evol/conditions.py:
--------------------------------------------------------------------------------
1 | from time import monotonic
2 | from typing import Callable, Optional, TYPE_CHECKING
3 |
4 | from evol.exceptions import StopEvolution
5 |
6 | if TYPE_CHECKING:
7 | from evol.population import BasePopulation
8 |
9 |
10 | class Condition:
11 | """Stop the evolution until a condition is no longer met.
12 |
13 | :param condition: A function that accepts a Population and returns a boolean.
14 | If the condition does not evaluate to True, then the evolution is stopped.
15 | """
16 | conditions = set()
17 |
18 | def __init__(self, condition: Optional[Callable[['BasePopulation'], bool]]):
19 | self.condition = condition
20 |
21 | def __enter__(self):
22 | self.conditions.add(self)
23 | return self
24 |
25 | def __exit__(self, exc_type, exc_val, exc_tb):
26 | self.conditions.remove(self)
27 |
28 | def __call__(self, population: 'BasePopulation') -> None:
29 | if self.condition and not self.condition(population):
30 | raise StopEvolution()
31 |
32 | @classmethod
33 | def check(cls, population: 'BasePopulation'):
34 | for condition in cls.conditions:
35 | condition(population)
36 |
37 |
38 | class MinimumProgress(Condition):
39 | """Stop the evolution if not enough progress is made.
40 |
41 | This condition stops the evolution if the best documented fitness
42 | does not improve enough within a given number of iterations.
43 |
44 | :param window: Number of iterations in which the minimum improvement must be made.
45 | :param change: Require more change in fitness than this value.
46 | Defaults to 0, meaning any change is good enough.
47 | """
48 |
49 | def __init__(self, window: int, change: float = 0):
50 | super().__init__(condition=None)
51 | self._history = []
52 | self.change = change
53 | self.window = window
54 |
55 | def __call__(self, population: 'BasePopulation') -> None:
56 | self._history = self._history[-self.window:]
57 | self._history.append(population.evaluate(lazy=True).documented_best.fitness)
58 | if len(self._history) > self.window and abs(self._history[0] - self._history[-1]) <= self.change:
59 | raise StopEvolution()
60 |
61 |
62 | class TimeLimit(Condition):
63 | """Stop the evolution after a given amount of time.
64 |
65 | This condition stops the evolution after a given amount of time
66 | has elapsed. Note that the time is only checked between iterations.
67 | If your iterations take long, the evolution may potentially run
68 | for much longer than anticipated.
69 |
70 | :param seconds: The time in seconds that the evolution may run.
71 | """
72 |
73 | def __init__(self, seconds: float):
74 | super().__init__(condition=None)
75 | self.time = None
76 | self.seconds = seconds
77 |
78 | def __call__(self, population: 'BasePopulation'):
79 | if self.time is None:
80 | self.time = monotonic()
81 | elif monotonic() - self.time > self.seconds:
82 | raise StopEvolution()
83 |
--------------------------------------------------------------------------------
/evol/evolution.py:
--------------------------------------------------------------------------------
1 | """
2 | Evolution objects in `evol` are objects that describe how the
3 | evolutionary algorithm will change members of a population.
4 | Evolution objects contain the same methods as population objects
5 | but because an evolution is separate from a population you can
6 | play around with them more easily.
7 | """
8 |
9 | from copy import copy
10 | from typing import Any, Callable, Iterator, List, Optional, Sequence
11 |
12 | from evol import Individual
13 | from .step import CheckpointStep, CallbackStep, EvolutionStep
14 | from .step import EvaluationStep, MapStep, FilterStep
15 | from .step import SurviveStep, BreedStep, MutateStep, RepeatStep
16 |
17 |
18 | class Evolution:
19 | """Describes the process a Population goes through when evolving."""
20 |
21 | def __init__(self):
22 | self.chain: List[EvolutionStep] = []
23 |
24 | def __copy__(self) -> 'Evolution':
25 | result = Evolution()
26 | result.chain = copy(self.chain)
27 | return result
28 |
29 | def __iter__(self) -> Iterator[EvolutionStep]:
30 | return self.chain.__iter__()
31 |
32 | def __repr__(self) -> str:
33 | result = 'Evolution('
34 | for step in self:
35 | result += '\n ' + repr(step).replace('\n', '\n ')
36 | result += ')'
37 | return result.strip('\n')
38 |
39 | def evaluate(self, lazy: bool = False, name: Optional[str] = None) -> 'Evolution':
40 | """Add an evaluation step to the Evolution.
41 |
42 | This evaluates the fitness of all individuals. If lazy is True, the
43 | fitness is only evaluated when a fitness value is not yet known. In
44 | most situations adding an explicit evaluation step is not needed, as
45 | lazy evaluation is implicitly included in the steps that need it (most
46 | notably in the survive step).
47 |
48 | :param lazy: If True, do no re-evaluate the fitness if the fitness is known.
49 | :param name: Name of the evaluation step.
50 | :return: This Evolution with an additional step.
51 | """
52 | return self._add_step(EvaluationStep(name=name, lazy=lazy))
53 |
54 | def checkpoint(self,
55 | target: Optional[str] = None,
56 | method: str = 'pickle',
57 | name: Optional[str] = None,
58 | every: int = 1) -> 'Evolution':
59 | """Add a checkpoint step to the Evolution.
60 |
61 | :param target: Directory to write checkpoint to. If None, the Serializer default target is taken,
62 | which can be provided upon initialisation. Defaults to None.
63 | :param method: One of 'pickle' or 'json'. When 'json', the chromosomes need to be json-serializable.
64 | Defaults to 'pickle'.
65 | :param name: Name of the map step.
66 | :param every: Checkpoint once every 'every' iterations. Defaults to 1.
67 | """
68 | return self._add_step(CheckpointStep(name=name, target=target, method=method, every=every))
69 |
70 | def map(self, func: Callable[..., Individual], name: Optional[str] = None, **kwargs) -> 'Evolution':
71 | """Add a map step to the Evolution.
72 |
73 | This applies the provided function to each individual in the
74 | population, in place.
75 |
76 | :param func: Function to apply to the individuals in the population.
77 | :param name: Name of the map step.
78 | :param kwargs: Arguments to pass to the function.
79 | :return: This Evolution with an additional step.
80 | """
81 | return self._add_step(MapStep(name=name, func=func, **kwargs))
82 |
83 | def filter(self, func: Callable[..., bool], name: Optional[str] = None, **kwargs) -> 'Evolution':
84 | """Add a filter step to the Evolution.
85 |
86 | This filters the individuals in the population using the provided function.
87 |
88 | :param func: Function to filter the individuals in the population by.
89 | :param name: Name of the filter step.
90 | :param kwargs: Arguments to pass to the function.
91 | :return: This Evolution with an additional step.
92 | """
93 | return self._add_step(FilterStep(name=name, func=func, **kwargs))
94 |
95 | def survive(self,
96 | fraction: Optional[float] = None,
97 | n: Optional[int] = None,
98 | luck: bool = False,
99 | name: Optional[str] = None,
100 | evaluate: bool = True) -> 'Evolution':
101 | """Add a survive step to the Evolution.
102 |
103 | This filters the individuals in the population according to fitness.
104 |
105 | :param fraction: Fraction of the original population that survives.
106 | Defaults to None.
107 | :param n: Number of individuals of the population that survive.
108 | Defaults to None.
109 | :param luck: If True, individuals randomly survive (with replacement!)
110 | with chances proportional to their fitness. Defaults to False.
111 | :param name: Name of the filter step.
112 | :param evaluate: If True, add a lazy evaluate step before the survive step.
113 | Defaults to True.
114 | :return: This Evolution with an additional step.
115 | """
116 | if evaluate:
117 | after_evaluate = self.evaluate(lazy=True)
118 | else:
119 | after_evaluate = self
120 | return after_evaluate._add_step(SurviveStep(name=name, fraction=fraction, n=n, luck=luck))
121 |
122 | def breed(self,
123 | parent_picker: Callable[..., Sequence[Individual]],
124 | combiner: Callable,
125 | population_size: Optional[int] = None,
126 | name: Optional[str] = None,
127 | **kwargs) -> 'Evolution':
128 | """Add a breed step to the Evolution.
129 |
130 | Create new individuals by combining existing individuals.
131 |
132 | :param parent_picker: Function that selects parents.
133 | :param combiner: Function that combines chromosomes into a new
134 | chromosome. Must be able to handle the number of chromosomes
135 | that the combiner returns.
136 | :param population_size: Intended population size after breeding.
137 | If None, take the previous intended population size.
138 | Defaults to None.
139 | :param name: Name of the breed step.
140 | :param kwargs: Kwargs to pass to the parent_picker and combiner.
141 | Arguments are only passed to the functions if they accept them.
142 | :return: self
143 | """
144 | return self._add_step(BreedStep(name=name, parent_picker=parent_picker, combiner=combiner,
145 | population_size=population_size, **kwargs))
146 |
147 | def mutate(self,
148 | mutate_function: Callable[..., Any],
149 | probability: float = 1.0,
150 | elitist: bool = False,
151 | name: Optional[str] = None,
152 | **kwargs) -> 'Evolution':
153 | """Add a mutate step to the Evolution.
154 |
155 | This mutates the chromosome of each individual.
156 |
157 | :param mutate_function: Function that accepts a chromosome and returns
158 | a mutated chromosome.
159 | :param probability: Probability that the individual mutates.
160 | The function is only applied in the given fraction of cases.
161 | Defaults to 1.0.
162 | :param elitist: If True, do not mutate the current best individual(s).
163 | Note that this only applies to evaluated individuals. Any unevaluated
164 | individual will be treated as normal.
165 | Defaults to False.
166 | :param name: Name of the mutate step.
167 | :param kwargs: Kwargs to pass to the parent_picker and combiner.
168 | Arguments are only passed to the functions if they accept them.
169 | :return: self
170 | """
171 | return self._add_step(MutateStep(name=name, probability=probability, elitist=elitist,
172 | mutate_function=mutate_function, **kwargs))
173 |
174 | def repeat(self, evolution: 'Evolution', n: int = 1, name: Optional[str] = None,
175 | grouping_function: Optional[Callable] = None, **kwargs) -> 'Evolution':
176 | """Add an evolution as a step to this evolution.
177 |
178 | This will add a step to the evolution that repeats another evolution
179 | several times. Optionally this step can be performed in groups.
180 |
181 | Note: if your population uses multiple concurrent workers and you use grouping,
182 | any callbacks inside the evolution you apply here may not have the desired effect.
183 |
184 | :param evolution: Evolution to apply.
185 | :param n: Number of times to perform the evolution. Defaults to 1.
186 | :param name: Name of the repeat step.
187 | :param grouping_function: Optional function to use for grouping the population.
188 | You can find built-in grouping functions in evol.helpers.groups.
189 | :param kwargs: Kwargs to pass to the grouping function, for example n_groups.
190 | :return: self
191 | """
192 | return self._add_step(RepeatStep(name=name, evolution=evolution, n=n,
193 | grouping_function=grouping_function, **kwargs))
194 |
195 | def callback(self, callback_function: Callable[..., Any],
196 | every: int = 1, name: Optional[str] = None, **kwargs) -> 'Evolution':
197 | """Call a function as a step in this evolution.
198 |
199 | This will call the provided function with the population as argument.
200 |
201 | Note that you can raise evol.exceptions.StopEvolution from within the
202 | callback to stop further evolution.
203 |
204 | :param callback_function: Function to call.
205 | :param every: Only call the function once per `every` iterations.
206 | Defaults to 1; every iteration.
207 | :param name: Name of the callback step.
208 | :return: self
209 | """
210 | return self._add_step(CallbackStep(name=name, every=every, callback_function=callback_function, **kwargs))
211 |
212 | def _add_step(self, step: EvolutionStep) -> 'Evolution':
213 | result = copy(self)
214 | result.chain.append(step)
215 | return result
216 |
--------------------------------------------------------------------------------
/evol/exceptions.py:
--------------------------------------------------------------------------------
1 | class PopulationIsNotEvaluatedException(RuntimeError):
2 | pass
3 |
4 |
5 | class StopEvolution(Exception):
6 | pass
7 |
--------------------------------------------------------------------------------
/evol/helpers/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Helpers in `evol` are functions that help you when you are
3 | designing algorithms. We archive these helping functions per usecase.
4 |
5 | """
--------------------------------------------------------------------------------
/evol/helpers/combiners/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/evol/helpers/combiners/__init__.py
--------------------------------------------------------------------------------
/evol/helpers/combiners/permutation.py:
--------------------------------------------------------------------------------
1 | from itertools import islice, tee
2 | from random import choice
3 | from typing import Any, Tuple
4 |
5 | from .utils import select_node, construct_neighbors, identify_cycles, cycle_parity
6 | from ..utils import select_partition
7 |
8 |
9 | def order_one_crossover(parent_1: Tuple, parent_2: Tuple) -> Tuple:
10 | """Combine two chromosomes using order-1 crossover.
11 |
12 | http://www.rubicite.com/Tutorials/GeneticAlgorithms/CrossoverOperators/Order1CrossoverOperator.aspx
13 |
14 | :param parent_1: First parent.
15 | :param parent_2: Second parent.
16 | :return: Child chromosome.
17 | """
18 | start, end = select_partition(len(parent_1))
19 | selected_partition = parent_1[start:end + 1]
20 | remaining_elements = filter(lambda element: element not in selected_partition, parent_2)
21 | return tuple(islice(remaining_elements, 0, start)) + selected_partition + tuple(remaining_elements)
22 |
23 |
24 | def edge_recombination(*parents: Tuple) -> Tuple:
25 | """Combine multiple chromosomes using edge recombination.
26 |
27 | http://www.rubicite.com/Tutorials/GeneticAlgorithms/CrossoverOperators/EdgeRecombinationCrossoverOperator.aspx
28 |
29 | :param parents: Chromosomes to combine.
30 | :return: Child chromosome.
31 | """
32 | return tuple(select_node(
33 | start_node=choice([chromosome[0] for chromosome in parents]),
34 | neighbors=construct_neighbors(*parents)
35 | ))
36 |
37 |
38 | def cycle_crossover(parent_1: Tuple, parent_2: Tuple) -> Tuple[Tuple[Any, ...], ...]:
39 | """Combine two chromosomes using cycle crossover.
40 |
41 | http://www.rubicite.com/Tutorials/GeneticAlgorithms/CrossoverOperators/CycleCrossoverOperator.aspx
42 |
43 | :param parent_1: First parent.
44 | :param parent_2: Second parent.
45 | :return: Child chromosome.
46 | """
47 | cycles = identify_cycles(parent_1, parent_2)
48 | parity = cycle_parity(cycles=cycles)
49 | it_a, it_b = tee((b, a) if parity[i] else (a, b) for i, (a, b) in enumerate(zip(parent_1, parent_2)))
50 | yield tuple(x[0] for x in it_a)
51 | yield tuple(y[1] for y in it_b)
52 |
--------------------------------------------------------------------------------
/evol/helpers/combiners/utils.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 | from itertools import tee, islice, cycle
3 |
4 | from random import choice
5 | from typing import Iterable, Generator, Any, Set, List, Dict, Tuple
6 |
7 |
8 | def construct_neighbors(*chromosome: Tuple[Any]) -> defaultdict:
9 | result = defaultdict(set)
10 | for element in chromosome:
11 | for x, y in _neighbors_in(element):
12 | result[x].add(y)
13 | result[y].add(x)
14 | return result
15 |
16 |
17 | def _neighbors_in(x: Tuple[Any], cyclic=True) -> Iterable[Tuple[Any, Any]]:
18 | a, b = tee(islice(cycle(x), 0, len(x) + (1 if cyclic else 0)))
19 | next(b, None)
20 | return zip(a, b)
21 |
22 |
23 | def _remove_from_neighbors(neighbors, node):
24 | del neighbors[node]
25 | for _, element in neighbors.items():
26 | element.difference_update({node})
27 |
28 |
29 | def select_node(start_node: Any, neighbors: defaultdict) -> Generator[Any, None, None]:
30 | node = start_node
31 | yield node
32 | while len(neighbors) > 1:
33 | options = neighbors[node]
34 | _remove_from_neighbors(neighbors, node)
35 | if len(options) > 0:
36 | min_len = min([len(neighbors[option]) for option in options])
37 | node = choice([option for option in options if len(neighbors[option]) == min_len])
38 | else:
39 | node = choice(list(neighbors.keys()))
40 | yield node
41 |
42 |
43 | def identify_cycles(chromosome_1: Tuple[Any], chromosome_2: Tuple[Any]) -> List[Set[int]]:
44 | """Identify all cycles between the chromosomes.
45 |
46 | A cycle is found by following this procedure: given an index, look up the
47 | value in the first chromosome. Then find the index of that value in the
48 | second chromosome. Repeat, until one returns to the original index.
49 |
50 | :param chromosome_1: First chromosome.
51 | :param chromosome_2: Second chromosome.
52 | :return: A list of cycles.
53 | """
54 | indices = set(range(len(chromosome_1)))
55 | cycles = []
56 | while len(indices) > 0:
57 | next_cycle = _identify_cycle(chromosome_1=chromosome_1, chromosome_2=chromosome_2, start_index=min(indices))
58 | indices.difference_update(next_cycle)
59 | cycles.append(next_cycle)
60 | return cycles
61 |
62 |
63 | def _identify_cycle(chromosome_1: Tuple[Any], chromosome_2: Tuple[Any], start_index: int = 0) -> Set[int]:
64 | """Identify a cycle between the chromosomes starting at the provided index.
65 |
66 | A cycle is found by following this procedure: given an index, look up the
67 | value in the first chromosome. Then find the index of that value in the
68 | second chromosome. Repeat, until one returns to the original index.
69 |
70 | :param chromosome_1: First chromosome.
71 | :param chromosome_2: Second chromosome.
72 | :param start_index: Index to start. Defaults to 0.
73 | :return: The set of indices in the identified cycle.
74 | """
75 | indices = set()
76 | index = start_index
77 | while index not in indices:
78 | indices.add(index)
79 | value = chromosome_1[index]
80 | index = chromosome_2.index(value)
81 | return indices
82 |
83 |
84 | def cycle_parity(cycles: List[Set[int]]) -> Dict[int, bool]:
85 | """Create a dictionary with the cycle parity of each index.
86 |
87 | Indices in all odd cycles have parity False, while
88 | indices in even cycles have parity True."""
89 | return {index: bool(i % 2) for i, c in enumerate(cycles) for index in c}
90 |
--------------------------------------------------------------------------------
/evol/helpers/groups.py:
--------------------------------------------------------------------------------
1 | from random import shuffle
2 | from typing import List
3 |
4 | from evol import Individual
5 | from evol.exceptions import PopulationIsNotEvaluatedException
6 |
7 | """
8 | Below are functions that allocate individuals to the
9 | island populations. It will be passed a list of individuals plus
10 | the kwargs passed to this method, and must return a list of lists
11 | of integers, each sub-list representing an island and the integers
12 | representing the index of an individual in the list. Each island
13 | must contain at least one individual, and individual may be copied
14 | to multiple islands.
15 | """
16 |
17 |
18 | def group_duplicate(individuals: List[Individual], n_groups: int = 4) -> List[List[int]]:
19 | """
20 | Group individuals into groups that each contain all individuals.
21 |
22 | :param individuals: List of individuals to group.
23 | :param n_groups: Number of groups to make.
24 | :return: List of lists of ints
25 | """
26 | return [list(range(len(individuals))) for _ in range(n_groups)]
27 |
28 |
29 | def group_random(individuals: List[Individual], n_groups: int = 4) -> List[List[int]]:
30 | """
31 | Group individuals randomly into groups of roughly equal size.
32 |
33 | :param individuals: List of individuals to group.
34 | :param n_groups: Number of groups to make.
35 | :return: List of lists of ints
36 | """
37 | indexes = list(range(len(individuals)))
38 | shuffle(indexes)
39 | return [indexes[i::n_groups] for i in range(n_groups)]
40 |
41 |
42 | def group_stratified(individuals: List[Individual], n_groups: int = 4) -> List[List[int]]:
43 | """
44 | Group individuals into groups of roughly equal size in a stratified manner.
45 |
46 | This function groups such that each group contains individuals of
47 | higher as well as lower fitness. This requires the individuals to have a fitness.
48 |
49 | :param individuals: List of individuals to group.
50 | :param n_groups: Number of groups to make.
51 | :return: List of lists of ints
52 | """
53 | _ensure_evaluated(individuals)
54 | indexes = list(map(
55 | lambda index_and_individual: index_and_individual[0],
56 | sorted(enumerate(individuals), key=lambda index_and_individual: index_and_individual[1].fitness)
57 | ))
58 | return [indexes[i::n_groups] for i in range(n_groups)]
59 |
60 |
61 | def _ensure_evaluated(individuals: List[Individual]):
62 | """
63 | Helper function to ensure individuals are evaluated.
64 |
65 | :param individuals: List of individuals
66 | :raises RuntimeError: When at least one of the individuals is not evaluated.
67 | """
68 | for individual in individuals:
69 | if individual.fitness is None:
70 | raise PopulationIsNotEvaluatedException('Population must be evaluated.')
71 |
--------------------------------------------------------------------------------
/evol/helpers/mutators/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/evol/helpers/mutators/__init__.py
--------------------------------------------------------------------------------
/evol/helpers/mutators/permutation.py:
--------------------------------------------------------------------------------
1 | from random import sample
2 | from typing import Any, Tuple
3 |
4 | from ..utils import select_partition
5 |
6 |
7 | def inversion(chromosome: Tuple[Any, ...], min_size: int = 2, max_size: int = None) -> Tuple[Any, ...]:
8 | """Mutate a chromosome using inversion.
9 |
10 | Inverts a random partition of the chromosome.
11 |
12 | :param chromosome: Original chromosome.
13 | :param min_size: Minimum partition size. Defaults to 2.
14 | :param max_size: Maximum partition size. Defaults to length - 1.
15 | :return: Mutated chromosome.
16 | """
17 | start, end = select_partition(len(chromosome), min_size, max_size)
18 | return chromosome[:start] + tuple(reversed(chromosome[start:end])) + chromosome[end:]
19 |
20 |
21 | def swap_elements(chromosome: Tuple[Any, ...]) -> Tuple[Any, ...]:
22 | """Randomly swap two elements of the chromosome.
23 |
24 | :param chromosome: Original chromosome.
25 | :return: Mutated chromosome.
26 | """
27 | result = list(chromosome)
28 | index_1, index_2 = sample(range(len(chromosome)), 2)
29 | result[index_1], result[index_2] = result[index_2], result[index_1]
30 | return tuple(result)
31 |
--------------------------------------------------------------------------------
/evol/helpers/pickers.py:
--------------------------------------------------------------------------------
1 | from typing import Sequence, Tuple
2 |
3 | from random import choice
4 |
5 | from evol import Individual
6 |
7 |
8 | def pick_random(parents: Sequence[Individual], n_parents: int = 2) -> Tuple:
9 | """Randomly selects parents with replacement
10 |
11 | Accepted arguments:
12 | n_parents: Number of parents to select. Defaults to 2.
13 | """
14 | return tuple(choice(parents) for _ in range(n_parents))
15 |
--------------------------------------------------------------------------------
/evol/helpers/utils.py:
--------------------------------------------------------------------------------
1 | from random import randint
2 | from typing import Tuple
3 |
4 |
5 | def select_partition(length: int, min_size: int = 1, max_size: int = None) -> Tuple[int, int]:
6 | """Select a partition of a chromosome.
7 |
8 | :param length: Length of the chromosome.
9 | :param min_size: Minimum length of the partition. Defaults to 1.
10 | :param max_size: Maximum length of the partition. Defaults to length - 1.
11 | :return: Start and end index of the partition.
12 | """
13 | partition_size = randint(min_size, length - 1 if max_size is None else max_size)
14 | partition_start = randint(0, length - partition_size)
15 | return partition_start, partition_start + partition_size
16 |
17 |
18 | def rotating_window(arr):
19 | """rotating_window([1,2,3,4]) -> [(4,1), (1,2), (2,3), (3,4)]"""
20 | for i, city in enumerate(arr):
21 | yield arr[i - 1], arr[i]
22 |
23 |
24 | def sliding_window(arr):
25 | """sliding_window([1,2,3,4]) -> [(1,2), (2,3), (3,4)]"""
26 | for i, city in enumerate(arr[:-1]):
27 | yield arr[i], arr[i + 1]
28 |
--------------------------------------------------------------------------------
/evol/individual.py:
--------------------------------------------------------------------------------
1 | """
2 | Individual objects in `evol` are a wrapper around a chromosome.
3 | Internally we work with individuals because that allows us to
4 | separate the fitness calculation from the data structure. This
5 | saves a lot of CPU power.
6 | """
7 |
8 | from random import random
9 | from typing import Any, Callable, Optional
10 | from uuid import uuid4
11 |
12 |
13 | class Individual:
14 | """Represents an individual in a population. The individual has a chromosome.
15 |
16 | :param chromosome: The chromosome of the individual.
17 | :param fitness: The fitness of the individual, or None.
18 | Defaults to None.
19 | """
20 |
21 | def __init__(self, chromosome: Any, fitness: Optional[float] = None):
22 | self.age = 0
23 | self.chromosome = chromosome
24 | self.fitness = fitness
25 | self.id = f"{str(uuid4())[:6]}"
26 |
27 | def __repr__(self):
28 | return f""
29 |
30 | @classmethod
31 | def from_dict(cls, data: dict) -> 'Individual':
32 | """Load an Individual from a dictionary.
33 |
34 | :param data: Dictionary containing the keys 'age', 'chromosome', 'fitness' and 'id'.
35 | :return: Individual
36 | """
37 | result = cls(chromosome=data['chromosome'], fitness=data['fitness'])
38 | result.age = data['age']
39 | result.id = data['id']
40 | return result
41 |
42 | def __post_evaluate(self, result):
43 | self.fitness = result
44 |
45 | def evaluate(self, eval_function: Callable[..., float], lazy: bool = False):
46 | """Evaluate the fitness of the individual.
47 |
48 | :param eval_function: Function that reduces a chromosome to a fitness.
49 | :param lazy: If True, do no re-evaluate the fitness if the fitness is known.
50 | """
51 | if self.fitness is None or not lazy:
52 | self.fitness = eval_function(self.chromosome)
53 |
54 | def mutate(self, mutate_function: Callable[..., Any], probability: float = 1.0, **kwargs):
55 | """Mutate the chromosome of the individual.
56 |
57 | :param mutate_function: Function that accepts a chromosome and returns a mutated chromosome.
58 | :param probability: Probability that the individual mutates.
59 | The function is only applied in the given fraction of cases.
60 | Defaults to 1.0.
61 | :param kwargs: Arguments to pass to the mutation function.
62 | """
63 | if probability == 1.0 or random() < probability:
64 | self.chromosome = mutate_function(self.chromosome, **kwargs)
65 | self.fitness = None
66 |
--------------------------------------------------------------------------------
/evol/logger.py:
--------------------------------------------------------------------------------
1 | """
2 | Loggers help keep track of the workings of your evolutionary algorithm. By
3 | default, each Population is initialized with a BaseLogger, which you can use
4 | by using the .log() method of the population. If you want more complex
5 | behaviour, you can supply another logger to the Population on initialisation.
6 | """
7 | import datetime as dt
8 | import os
9 | import json
10 | import logging
11 | import sys
12 | import uuid
13 |
14 | from evol.exceptions import PopulationIsNotEvaluatedException
15 | from evol.population import BasePopulation
16 |
17 |
18 | class BaseLogger:
19 | """
20 | The `evol.BaseLogger` is the most basic logger in evol.
21 | You can supply it to a population so that the population
22 | knows how to handle the `.log()` verb.
23 | """
24 |
25 | def __init__(self, target=None, stdout=False, fmt='%(asctime)s,%(message)s'):
26 | self.file = target
27 | if target is not None:
28 | if not os.path.exists(os.path.split(target)[0]):
29 | raise RuntimeError(f"path to target {os.path.split(target)[0]} does not exist!")
30 | formatter = logging.Formatter(fmt=fmt, datefmt='%Y-%m-%d %H:%M:%S')
31 | self.logger = logging.getLogger(name=f"{uuid.uuid4()}")
32 | if not self.logger.handlers:
33 | # we do this extra step because loggers can behave in strange ways otherwise
34 | # https://navaspot.wordpress.com/2015/09/22/same-log-messages-multiple-times-in-python-issue/
35 | if target:
36 | file_handler = logging.FileHandler(filename=target)
37 | file_handler.setFormatter(fmt=formatter)
38 | self.logger.addHandler(file_handler)
39 | if stdout:
40 | stream_handler = logging.StreamHandler(stream=sys.stdout)
41 | stream_handler.setFormatter(fmt=formatter)
42 | self.logger.addHandler(stream_handler)
43 | self.logger.setLevel(level=logging.INFO)
44 |
45 | @staticmethod
46 | def check_population(population: BasePopulation) -> None:
47 | if not population.is_evaluated:
48 | raise PopulationIsNotEvaluatedException('Population must be evaluated when logging.')
49 |
50 | def log(self, population, **kwargs):
51 | """
52 | The logger method of the Logger object determines what will be logged.
53 | :param population: `evol.Population` object
54 | :return: nothing, it merely logs to a file and perhaps stdout
55 | """
56 | self.check_population(population)
57 | values = ','.join([str(item) for item in kwargs.values()])
58 | if values != '':
59 | values = f',{values}'
60 | for i in population:
61 | self.logger.info(f'{population.id},{i.id},{i.fitness}' + values)
62 |
63 |
64 | class SummaryLogger(BaseLogger):
65 | """
66 | The `evol.SummaryLogger` merely logs statistics per population and nothing else.
67 | You are still able to log to stdout as well.
68 | """
69 |
70 | def log(self, population, **kwargs):
71 | self.check_population(population)
72 | values = ','.join([str(item) for item in kwargs.values()])
73 | if values != '':
74 | values = f',{values}'
75 | fitnesses = [i.fitness for i in population]
76 | self.logger.info(f'{min(fitnesses)},{sum(fitnesses) / len(fitnesses)},{max(fitnesses)}' + values)
77 |
78 |
79 | class MultiLogger:
80 | """
81 | The `evol.Multilogger` is a logger object that can handle writing to two files.
82 | It is here for demonstration purposes to show how you could customize the logging.
83 | The only thing that matters is that all logging is handled by the `.log()`
84 | call. So we are free to record to multiple files if we want as well. This is
85 | not per se best practice but it would work.
86 | """
87 |
88 | def __init__(self, file_individuals, file_population):
89 | self.file_individuals = file_individuals
90 | self.file_population = file_population
91 |
92 | def log(self, population, **kwargs):
93 | """
94 | The logger method of the Logger object determines what will be logged.
95 | :param population: population to log
96 | :return: generator of strings to be handled
97 | """
98 | ind_generator = (f'{dt.datetime.now()},{population.id},{i.id},{i.fitness}' for i in population)
99 | fitnesses = [i.fitness for i in population]
100 | data = {
101 | 'ts': str(dt.datetime.now()),
102 | 'mean_ind': sum(fitnesses) / len(fitnesses),
103 | 'min_ind': min(fitnesses),
104 | 'max_ind': max(fitnesses)
105 | }
106 | dict_to_log = {**kwargs, **data}
107 | self.handle(ind_generator, dict_to_log)
108 |
109 | def handle(self, ind_generator, dict_to_log):
110 | """
111 | The handler method of the Logger object determines how it will be logged.
112 | In this case we print if there is no file and we append to a file otherwise.
113 | """
114 | with open(self.file_population, 'a') as f:
115 | f.write(json.dumps(dict_to_log))
116 | with open(self.file_population, 'a') as f:
117 | f.writelines(ind_generator)
118 |
--------------------------------------------------------------------------------
/evol/problems/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/evol/problems/__init__.py
--------------------------------------------------------------------------------
/evol/problems/functions/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | The `evol.problems.functions` part of the library contains
3 | simple problem instances that do with known math functions.
4 |
5 | The functions in here are typically inspired from wikipedia:
6 | https://en.wikipedia.org/wiki/Test_functions_for_optimization
7 | """
8 |
9 | from .variableinput import Rosenbrock, Sphere, Rastrigin
--------------------------------------------------------------------------------
/evol/problems/functions/variableinput.py:
--------------------------------------------------------------------------------
1 | import math
2 | from typing import Sequence
3 |
4 | from evol.helpers.utils import sliding_window
5 | from evol.problems.problem import Problem
6 |
7 |
8 | class FunctionProblem(Problem):
9 | def __init__(self, size=2):
10 | self.size = size
11 |
12 | def check_solution(self, solution: Sequence[float]) -> Sequence[float]:
13 | if len(solution) > self.size:
14 | raise ValueError(f"{self.__class__.__name__} has size {self.size}, \
15 | got solution of size: {len(solution)}")
16 | return solution
17 |
18 | def value(self, solution):
19 | return sum(solution)
20 |
21 | def eval_function(self, solution: Sequence[float]) -> float:
22 | self.check_solution(solution)
23 | return self.value(solution)
24 |
25 |
26 | class Sphere(FunctionProblem):
27 | def value(self, solution: Sequence[float]) -> float:
28 | """
29 | The optimal value can be found when a sequence of zeros is given.
30 | :param solution: a sequence of x_i values
31 | :return: the value of the Sphere function
32 | """
33 | return sum([_**2 for _ in solution])
34 |
35 |
36 | class Rosenbrock(FunctionProblem):
37 | def value(self, solution: Sequence[float]) -> float:
38 | """
39 | The optimal value can be found when a sequence of ones is given.
40 | :param solution: a sequence of x_i values
41 | :return: the value of the Rosenbrock function
42 | """
43 | result = 0
44 | for x_i, x_j in sliding_window(solution):
45 | result += 100*(x_j - x_i**2)**2 + (1 - x_i)**2
46 | return result
47 |
48 |
49 | class Rastrigin(FunctionProblem):
50 | def value(self, solution: Sequence[float]) -> float:
51 | """
52 | The optimal value can be found when a sequence of zeros is given.
53 | :param solution: a sequence of x_i values
54 | :return: the value of the Rosenbrock function
55 | """
56 | return (10 * self.size) + sum([_**2 - 10 * math.cos(2*math.pi*_) for _ in solution])
57 |
--------------------------------------------------------------------------------
/evol/problems/problem.py:
--------------------------------------------------------------------------------
1 | from abc import ABCMeta, abstractmethod
2 |
3 |
4 | class Problem(metaclass=ABCMeta):
5 |
6 | @abstractmethod
7 | def eval_function(self, solution):
8 | raise NotImplementedError
9 |
--------------------------------------------------------------------------------
/evol/problems/routing/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | The `evol.problems.routing` part of the library contains
3 | simple problem instances that do with routing problems. These
4 | are meant to be used for education and training purposes and
5 | these problems are typically good starting points if you want
6 | to play with the library.
7 | """
8 |
9 | from .tsp import TSPProblem
10 | from .magicsanta import MagicSanta
11 |
--------------------------------------------------------------------------------
/evol/problems/routing/coordinates.py:
--------------------------------------------------------------------------------
1 | united_states_capitols = [
2 | (32.361538, -86.279118, "Montgomery", "Alabama"),
3 | (58.301935, -134.419740, "Juneau", "Alaska"),
4 | (33.448457, -112.073844, "Phoenix", "Arizona"),
5 | (34.736009, -92.331122, "Little Rock", "Arkansas"),
6 | (38.555605, -121.468926, "Sacramento", "California"),
7 | (39.7391667, -104.984167, "Denver", "Colorado"),
8 | (41.767, -72.677, "Hartford", "Connectic"),
9 | (39.161921, -75.526755, "Dover", "Delaware"),
10 | (30.4518, -84.27277, "Tallahassee", "Florida"),
11 | (33.76, -84.39, "Atlanta", "Georgia"),
12 | (21.30895, -157.826182, "Honolulu", "Hawaii"),
13 | (43.613739, -116.237651, "Boise", "Idaho"),
14 | (39.783250, -89.650373, "Springfield", "Illinois"),
15 | (39.790942, -86.147685, "Indianapolis", "Indiana"),
16 | (41.590939, -93.620866, "Des Moines", "Iowa"),
17 | (39.04, -95.69, "Topeka", "Kansas"),
18 | (38.197274, -84.86311, "Frankfort", "Kentucky"),
19 | (30.45809, -91.140229, "Baton Rouge", "Louisiana"),
20 | (44.323535, -69.765261, "Augusta", "Maine"),
21 | (38.972945, -76.501157, "Annapolis", "Maryland"),
22 | (42.2352, -71.0275, "Boston", "Massachuset"),
23 | (42.7335, -84.5467, "Lansing", "Michigan"),
24 | (44.95, -93.094, "Saint Paul", "Minnesot"),
25 | (32.320, -90.207, "Jackson", "Mississip"),
26 | (38.572954, -92.189283, "Jefferson City", "Missouri"),
27 | (46.595805, -112.027031, "Helana", "Montana"),
28 | (40.809868, -96.675345, "Lincoln", "Nebraska"),
29 | (39.160949, -119.753877, "Carson City", "Nevada"),
30 | (43.220093, -71.549127, "Concord", "Hampshire"),
31 | (40.221741, -74.756138, "Trenton", "Jersey"),
32 | (35.667231, -105.964575, "Santa Fe", "Mexico"),
33 | (42.659829, -73.781339, "Albany", "York"),
34 | (35.771, -78.638, "Raleigh", "Car"),
35 | (48.813343, -100.779004, "Bismarck", "Dakota"),
36 | (39.962245, -83.000647, "Columbus", "Ohio"),
37 | (35.482309, -97.534994, "Oklahoma City", "Oklahoma"),
38 | (44.931109, -123.029159, "Salem", "Oregon"),
39 | (40.269789, -76.875613, "Harrisburg", "Pennsylvania"),
40 | (41.82355, -71.422132, "Providence", "Island"),
41 | (34.000, -81.035, "Columbia", "Car"),
42 | (44.367966, -100.336378, "Pierre", "Dakota"),
43 | (36.165, -86.784, "Nashville", "Tennessee"),
44 | (30.266667, -97.75, "Austin", "Texas"),
45 | (40.7547, -111.892622, "Salt Lake City", "Utah"),
46 | (44.26639, -72.57194, "Montpelier", "Vermont"),
47 | (37.54, -77.46, "Richmond", "Virgini"),
48 | (47.042418, -122.893077, "Olympia", "Washington"),
49 | (38.349497, -81.633294, "Charleston", "Virginia"),
50 | (43.074722, -89.384444, "Madison", "Wisconsin"),
51 | (41.145548, -104.802042, "Cheyenne", "Wyoming")]
52 |
--------------------------------------------------------------------------------
/evol/problems/routing/magicsanta.py:
--------------------------------------------------------------------------------
1 | import math
2 | from collections import Counter
3 | from itertools import chain
4 | from typing import List, Union
5 |
6 | from evol.helpers.utils import sliding_window
7 | from evol.problems.problem import Problem
8 |
9 |
10 | class MagicSanta(Problem):
11 | def __init__(self, city_coordinates, home_coordinate, gift_weight=None, sleigh_weight=1):
12 | """
13 | This problem is based on this kaggle competition:
14 | https://www.kaggle.com/c/santas-stolen-sleigh#evaluation.
15 | :param city_coordinates: List of tuples containing city coordinates.
16 | :param home_coordinate: Tuple containing coordinate of home base.
17 | :param gift_weight: Vector of weights per gift associated with cities.
18 | :param sleigh_weight: Weight of the sleight.
19 | """
20 | self.coordinates = city_coordinates
21 | self.home_coordinate = home_coordinate
22 | self.gift_weight = gift_weight
23 | if gift_weight is None:
24 | self.gift_weight = [1 for _ in city_coordinates]
25 | self.sleigh_weight = sleigh_weight
26 |
27 | @staticmethod
28 | def distance(coord_a, coord_b):
29 | return math.sqrt(sum([(z[0] - z[1]) ** 2 for z in zip(coord_a, coord_b)]))
30 |
31 | def check_solution(self, solution: List[List[int]]):
32 | """
33 | Check if the solution for the problem is valid.
34 | :param solution: List of lists containing integers representing visited cities.
35 | :return: None, unless errors are raised.
36 | """
37 | set_visited = set(chain.from_iterable(solution))
38 | set_problem = set(range(len(self.coordinates)))
39 | if set_visited != set_problem:
40 | missing = set_problem.difference(set_visited)
41 | extra = set_visited.difference(set_problem)
42 | raise ValueError(f"Not all cities are visited! Missing: {missing} Extra: {extra}")
43 | city_counter = Counter(chain.from_iterable(solution))
44 | if max(city_counter.values()) > 1:
45 | double_cities = {key for key, value in city_counter.items() if value > 1}
46 | raise ValueError(f"Multiple occurrences found for cities: {double_cities}")
47 |
48 | def eval_function(self, solution: List[List[int]]) -> Union[float, int]:
49 | """
50 | Calculates the cost of the current solution for the TSP problem.
51 | :param solution: List of integers which refer to cities.
52 | :return:
53 | """
54 | self.check_solution(solution=solution)
55 | cost = 0
56 | for route in solution:
57 | total_route_weight = sum([self.gift_weight[t] for t in route]) + self.sleigh_weight
58 | distance = self.distance(self.home_coordinate, self.coordinates[route[0]])
59 | cost += distance * total_route_weight
60 | for t1, t2 in sliding_window(route):
61 | total_route_weight -= self.gift_weight[t1]
62 | city1 = self.coordinates[t1]
63 | city2 = self.coordinates[t2]
64 | cost += self.distance(city1, city2) * total_route_weight
65 | last_leg_distance = self.distance(self.coordinates[route[-1]], self.home_coordinate)
66 | cost += self.sleigh_weight * last_leg_distance
67 | return cost
68 |
--------------------------------------------------------------------------------
/evol/problems/routing/tsp.py:
--------------------------------------------------------------------------------
1 | import math
2 | from typing import List, Union
3 |
4 | from evol.problems.problem import Problem
5 | from evol.helpers.utils import rotating_window
6 |
7 |
8 | class TSPProblem(Problem):
9 | def __init__(self, distance_matrix):
10 | self.distance_matrix = distance_matrix
11 |
12 | @classmethod
13 | def from_coordinates(cls, coordinates: List[Union[tuple, list]]) -> 'TSPProblem':
14 | """
15 | Creates a distance matrix from a list of city coordinates.
16 | :param coordinates: An iterable that contains tuples or lists representing a x,y coordinate.
17 | :return: A list of lists containing the distances between cities.
18 | """
19 | res = [[0 for i in coordinates] for j in coordinates]
20 | for i, coord_i in enumerate(coordinates):
21 | for j, coord_j in enumerate(coordinates):
22 | dist = math.sqrt(sum([(z[0] - z[1])**2 for z in zip(coord_i[:2], coord_j[:2])]))
23 | res[i][j] = dist
24 | res[j][i] = dist
25 | return TSPProblem(distance_matrix=res)
26 |
27 | def check_solution(self, solution: List[int]):
28 | """
29 | Check if the solution for the TSP problem is valid.
30 | :param solution: List of integers which refer to cities.
31 | :return: None, unless errors are raised.
32 | """
33 | set_solution = set(solution)
34 | set_problem = set(range(len(self.distance_matrix)))
35 | if len(solution) > len(self.distance_matrix):
36 | raise ValueError("Solution is longer than number of towns!")
37 | if set_solution != set_problem:
38 | raise ValueError(f"Not all towns are visited! Am missing {set_problem.difference(set_solution)}")
39 |
40 | def eval_function(self, solution: List[int]) -> Union[float, int]:
41 | """
42 | Calculates the cost of the current solution for the TSP problem.
43 | :param solution: List of integers which refer to cities.
44 | :return:
45 | """
46 | self.check_solution(solution=solution)
47 | cost = 0
48 | for t1, t2 in rotating_window(solution):
49 | cost += self.distance_matrix[t1][t2]
50 | return cost
51 |
--------------------------------------------------------------------------------
/evol/serialization.py:
--------------------------------------------------------------------------------
1 | """
2 | Serializers help store (checkpoint) the state of your population during or
3 | after running your evolutionary algorithm. By default, each Population is
4 | initialized with a SimpleSerializer, which you can use to store the individuals
5 | in your population in pickle or json format using the .checkpoint() method of
6 | the population. Currently no other serializers are available.
7 | """
8 | import json
9 | import pickle
10 | from datetime import datetime
11 | from typing import List, Optional
12 |
13 | from os import listdir
14 | from os.path import isdir, exists, join
15 |
16 | from evol import Individual
17 |
18 |
19 | class SimpleSerializer:
20 | """The SimpleSerializer handles serialization to and from pickle and json.
21 |
22 | :param target: Default location (directory) to store checkpoint.
23 | This may be overridden in the `checkpoint` method. Defaults to None.
24 | """
25 |
26 | def __init__(self, target: Optional[str] = None):
27 | self.target = target
28 |
29 | def checkpoint(self, individuals: List[Individual], target: Optional[str] = None, method: str = 'pickle') -> None:
30 | """Checkpoint a list of individuals.
31 |
32 | :param individuals: List of individuals to checkpoint.
33 | :param target: Directory to write checkpoint to. If None, the Serializer default target is taken,
34 | which can be provided upon initialisation. Defaults to None.
35 | :param method: One of 'pickle' or 'json'. When 'json', the chromosomes need to be json-serializable.
36 | Defaults to 'pickle'.
37 | """
38 | filename = self._new_checkpoint_file(target=self.target if target is None else target, method=method)
39 | if method == 'pickle':
40 | with open(filename, 'wb') as pickle_file:
41 | pickle.dump(individuals, pickle_file)
42 | elif method == 'json':
43 | with open(filename, 'w') as json_file:
44 | json.dump([individual.__dict__ for individual in individuals], json_file)
45 | else:
46 | raise ValueError('Invalid checkpointing method "{}". Choose "pickle" or "json".'.format(method))
47 |
48 | def load(self, target: Optional[str] = None) -> List[Individual]:
49 | """Load a checkpoint.
50 |
51 | If path is a file, load that file. If it is a directory, load the most recent checkpoint.
52 | The checkpoint file must end with a '.json' or '.pkl' extension.
53 |
54 | :param target: Path to checkpoint directory or file.
55 | :return: List of individuals from checkpoint.
56 | """
57 | filename = self._find_checkpoint(self.target if target is None else target)
58 | if filename.endswith('.json'):
59 | with open(filename, 'r') as json_file:
60 | return [Individual.from_dict(d) for d in json.load(json_file)]
61 | elif filename.endswith('.pkl'):
62 | with open(filename, 'rb') as pickle_file:
63 | return pickle.load(pickle_file)
64 |
65 | @staticmethod
66 | def _new_checkpoint_file(target: str, method: str):
67 | """Generate a filename for a new checkpoint."""
68 | if target is None:
69 | raise ValueError('Serializer requires a target to checkpoint to.')
70 | if not isdir(target):
71 | raise FileNotFoundError('Cannot checkpoint to "{}": is not a directory.'.format(target))
72 | result = join(target, datetime.now().strftime("%Y%m%d-%H%M%S.%f") + ('.pkl' if method == 'pickle' else '.json'))
73 | if exists(result):
74 | raise FileExistsError('Cannot checkpoint to "{}": file exists.'.format(result))
75 | return result
76 |
77 | @classmethod
78 | def _find_checkpoint(cls, target: str):
79 | """Find the most recent checkpoint file."""
80 | if not exists(target):
81 | raise FileNotFoundError('Cannot load from "{}": file or directory does not exists.'.format(target))
82 | elif isdir(target):
83 | try:
84 | return join(target, max(filter(cls._has_valid_extension, listdir(path=target))))
85 | except ValueError:
86 | raise FileNotFoundError('Cannot load from "{}": directory contains no checkpoints.'.format(target))
87 | else:
88 | if not cls._has_valid_extension(target):
89 | raise ValueError('Invalid extension "{}": Was expecting ".pkl" or ".json".'.format(target))
90 | return target
91 |
92 | @staticmethod
93 | def _has_valid_extension(filename: str):
94 | """Check if a filename has a valid extension."""
95 | return filename.endswith('.pkl') or filename.endswith('.json')
96 |
--------------------------------------------------------------------------------
/evol/step.py:
--------------------------------------------------------------------------------
1 | from abc import ABCMeta, abstractmethod
2 | from typing import Callable, Optional, TYPE_CHECKING
3 |
4 | from evol.population import BasePopulation
5 |
6 | if TYPE_CHECKING:
7 | from evol.evolution import Evolution
8 |
9 |
10 | class EvolutionStep(metaclass=ABCMeta):
11 |
12 | def __init__(self, name: Optional[str], **kwargs):
13 | self.name = name
14 | self.kwargs = kwargs
15 |
16 | def __repr__(self):
17 | return f"{self.__class__.__name__}({self.name or ''})"
18 |
19 | @abstractmethod
20 | def apply(self, population: BasePopulation) -> BasePopulation:
21 | pass
22 |
23 |
24 | class EvaluationStep(EvolutionStep):
25 |
26 | def apply(self, population: BasePopulation) -> BasePopulation:
27 | return population.evaluate(**self.kwargs)
28 |
29 |
30 | class CheckpointStep(EvolutionStep):
31 |
32 | def __init__(self, name, every=1, **kwargs):
33 | EvolutionStep.__init__(self, name, **kwargs)
34 | self.count = 0
35 | self.every = every
36 |
37 | def apply(self, population: BasePopulation) -> BasePopulation:
38 | self.count += 1
39 | if self.count >= self.every:
40 | self.count = 0
41 | return population.checkpoint(**self.kwargs)
42 | return population
43 |
44 |
45 | class MapStep(EvolutionStep):
46 |
47 | def apply(self, population: BasePopulation) -> BasePopulation:
48 | return population.map(**self.kwargs)
49 |
50 |
51 | class FilterStep(EvolutionStep):
52 |
53 | def apply(self, population: BasePopulation) -> BasePopulation:
54 | return population.filter(**self.kwargs)
55 |
56 |
57 | class SurviveStep(EvolutionStep):
58 |
59 | def apply(self, population: BasePopulation) -> BasePopulation:
60 | return population.survive(**self.kwargs)
61 |
62 |
63 | class BreedStep(EvolutionStep):
64 |
65 | def apply(self, population: BasePopulation) -> BasePopulation:
66 | return population.breed(**self.kwargs)
67 |
68 |
69 | class MutateStep(EvolutionStep):
70 |
71 | def apply(self, population: BasePopulation) -> BasePopulation:
72 | return population.mutate(**self.kwargs)
73 |
74 |
75 | class RepeatStep(EvolutionStep):
76 |
77 | def __init__(self, name: str, evolution: 'Evolution', n: int,
78 | grouping_function: Optional[Callable] = None, **kwargs):
79 | super().__init__(name=name, **kwargs)
80 | self.evolution = evolution
81 | self.n = n
82 | self.grouping_function = grouping_function
83 |
84 | def apply(self, population: BasePopulation) -> BasePopulation:
85 | if self.grouping_function is None:
86 | if len(self.kwargs) > 0:
87 | raise ValueError(f'Unexpected argument(s) for non-grouped repeat step: {self.kwargs}')
88 | return population.evolve(evolution=self.evolution, n=self.n)
89 | else:
90 | return self._apply_grouped(population=population)
91 |
92 | def _apply_grouped(self, population: BasePopulation) -> BasePopulation:
93 | groups = population.group(grouping_function=self.grouping_function, **self.kwargs)
94 | if population.pool:
95 | results = population.pool.map(lambda group: group.evolve(evolution=self.evolution, n=self.n), groups)
96 | else:
97 | results = [group.evolve(evolution=self.evolution, n=self.n) for group in groups]
98 | return population.combine(*results, intended_size=population.intended_size, pool=population.pool)
99 |
100 | def __repr__(self):
101 | result = f"{self.__class__.__name__}({self.name or ''}) with evolution ({self.n}x):\n "
102 | result += repr(self.evolution).replace('\n', '\n ')
103 | return result
104 |
105 |
106 | class CallbackStep(EvolutionStep):
107 | def __init__(self, name, every: int = 1, **kwargs):
108 | EvolutionStep.__init__(self, name, **kwargs)
109 | self.count = 0
110 | self.every = every
111 |
112 | def apply(self, population: BasePopulation) -> BasePopulation:
113 | self.count += 1
114 | if self.count >= self.every:
115 | self.count = 0
116 | return population.callback(**self.kwargs)
117 | return population
118 |
--------------------------------------------------------------------------------
/evol/utils.py:
--------------------------------------------------------------------------------
1 | from inspect import signature
2 | from typing import List, Callable, Union, Sequence, Any, Generator
3 |
4 | from evol import Individual
5 |
6 |
7 | def offspring_generator(parents: List[Individual],
8 | parent_picker: Callable[..., Union[Individual, Sequence]],
9 | combiner: Callable[..., Any],
10 | **kwargs) -> Generator[Individual, None, None]:
11 | """Generator for offspring.
12 |
13 | This helps create the right number of offspring,
14 | especially in the case of of multiple offspring.
15 |
16 | :param parents: List of parents.
17 | :param parent_picker: Function that selects parents. Must accept a sequence of
18 | individuals and must return a single individual or a sequence of individuals.
19 | Must accept all kwargs passed (i.e. must be decorated by select_arguments).
20 | :param combiner: Function that combines chromosomes. Must accept a tuple of
21 | chromosomes and either return a single chromosome or yield multiple chromosomes.
22 | Must accept all kwargs passed (i.e. must be decorated by select_arguments).
23 | :param kwargs: Arguments
24 | :returns: Children
25 | """
26 | while True:
27 | # Obtain parent chromosomes
28 | selected_parents = parent_picker(parents, **kwargs)
29 | if isinstance(selected_parents, Individual):
30 | chromosomes = (selected_parents.chromosome,)
31 | else:
32 | chromosomes = tuple(individual.chromosome for individual in selected_parents)
33 | # Create children
34 | combined = combiner(*chromosomes, **kwargs)
35 | if isinstance(combined, Generator):
36 | for child in combined:
37 | yield Individual(chromosome=child)
38 | else:
39 | yield Individual(chromosome=combined)
40 |
41 |
42 | def select_arguments(func: Callable) -> Callable:
43 | """Decorate a function such that it accepts any keyworded arguments.
44 |
45 | The resulting function accepts any arguments, but only arguments that
46 | the original function accepts are passed. This allows keyworded
47 | arguments to be passed to multiple (decorated) functions, even if they
48 | do not (all) accept these arguments.
49 |
50 | :param func: Function to decorate.
51 | :return: Callable
52 | """
53 | def result(*args, **kwargs):
54 | try:
55 | return func(*args, **kwargs)
56 | except TypeError:
57 | return func(*args, **{k: v for k, v in kwargs.items() if k in signature(func).parameters})
58 |
59 | return result
60 |
--------------------------------------------------------------------------------
/examples/number_of_parents.py:
--------------------------------------------------------------------------------
1 | """
2 | There are a few worthwhile things to notice in this example:
3 |
4 | 1. you can pass hyperparams into functions from the `.breed` and `.mutate` step
5 | 2. the algorithm does not care how many parents it will use in the `breed` step
6 | """
7 |
8 | import random
9 | import math
10 | import argparse
11 |
12 | from evol import Population, Evolution
13 |
14 |
15 | def run_evolutionary(opt_value=1, population_size=100, n_parents=2, workers=1,
16 | num_iter=200, survival=0.5, noise=0.1, seed=42, verbose=True):
17 | random.seed(seed)
18 |
19 | def init_func():
20 | return (random.random() - 0.5) * 20 + 10
21 |
22 | def eval_func(x, opt_value=opt_value):
23 | return -((x - opt_value) ** 2) + math.cos(x - opt_value)
24 |
25 | def random_parent_picker(pop, n_parents):
26 | return [random.choice(pop) for i in range(n_parents)]
27 |
28 | def mean_parents(*parents):
29 | return sum(parents) / len(parents)
30 |
31 | def add_noise(chromosome, sigma):
32 | return chromosome + (random.random() - 0.5) * sigma
33 |
34 | pop = Population(chromosomes=[init_func() for _ in range(population_size)],
35 | eval_function=eval_func, maximize=True, concurrent_workers=workers).evaluate()
36 |
37 | evo = (Evolution()
38 | .survive(fraction=survival)
39 | .breed(parent_picker=random_parent_picker, combiner=mean_parents, n_parents=n_parents)
40 | .mutate(mutate_function=add_noise, sigma=noise)
41 | .evaluate())
42 |
43 | for i in range(num_iter):
44 | pop = pop.evolve(evo)
45 |
46 | if verbose:
47 | print(f"iteration:{i} best: {pop.current_best.fitness} worst: {pop.current_worst.fitness}")
48 |
49 |
50 | if __name__ == "__main__":
51 | parser = argparse.ArgumentParser(description='Run an example evol algorithm against a simple continuous function.')
52 | parser.add_argument('--opt-value', type=int, default=0,
53 | help='the true optimal value of the problem')
54 | parser.add_argument('--population-size', type=int, default=20,
55 | help='the number of candidates to start the algorithm with')
56 | parser.add_argument('--n-parents', type=int, default=2,
57 | help='the number of parents the algorithm with use to generate new indivuals')
58 | parser.add_argument('--num-iter', type=int, default=20,
59 | help='the number of evolutionary cycles to run')
60 | parser.add_argument('--survival', type=float, default=0.7,
61 | help='the fraction of individuals who will survive a generation')
62 | parser.add_argument('--noise', type=float, default=0.5,
63 | help='the amount of noise the mutate step will add to each individual')
64 | parser.add_argument('--seed', type=int, default=42,
65 | help='the random seed for all this')
66 | parser.add_argument('--workers', type=int, default=1,
67 | help='the number of workers to run the command in')
68 |
69 | args = parser.parse_args()
70 | run_evolutionary(opt_value=args.opt_value, population_size=args.population_size,
71 | n_parents=args.n_parents, num_iter=args.num_iter,
72 | noise=args.noise, seed=args.seed, workers=args.workers)
73 |
--------------------------------------------------------------------------------
/examples/population_demo.py:
--------------------------------------------------------------------------------
1 | import random
2 | from evol import Population
3 |
4 |
5 | def create_candidate():
6 | return random.random() - 0.5
7 |
8 |
9 | def func_to_optimise(x):
10 | return x*2
11 |
12 |
13 | def pick_random_parents(pop):
14 | return random.choice(pop)
15 |
16 |
17 | random.seed(42)
18 |
19 | pop1 = Population(chromosomes=[create_candidate() for _ in range(5)],
20 | eval_function=func_to_optimise, maximize=True)
21 |
22 | pop2 = Population.generate(init_function=create_candidate,
23 | eval_function=func_to_optimise,
24 | size=5,
25 | maximize=False)
26 |
27 |
28 | print("[i for i in pop1]:")
29 | print([i for i in pop1])
30 | print("[i.chromosome for i in pop1]:")
31 | print([i.chromosome for i in pop1])
32 | print("[i.fitness for i in pop1]:")
33 | print([i.fitness for i in pop1])
34 | print("[i.fitness for i in pop1.evaluate()]:")
35 |
36 |
37 | def produce_clone(parent):
38 | return parent
39 |
40 |
41 | def add_noise(x):
42 | return 0.1 * (random.random() - 0.5) + x
43 |
44 |
45 | print("[i.fitness for i in pop1.survive(n=3)]:")
46 | print([i.fitness for i in pop1.survive(n=3)])
47 | print("[i.fitness for i in pop1.survive(n=3).mutate(add_noise)]:")
48 | print([i.fitness for i in pop1.survive(n=3).mutate(add_noise)])
49 | print("[i.fitness for i in pop1.survive(n=3).mutate(add_noise).evaluate()]:")
50 | print([i.fitness for i in pop1.survive(n=3).mutate(add_noise).evaluate()])
51 |
--------------------------------------------------------------------------------
/examples/rock_paper_scissors.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from argparse import ArgumentParser
4 | from collections import Counter
5 | from random import choice, random, seed
6 | from typing import List
7 |
8 | from evol import Evolution, ContestPopulation
9 | from evol.helpers.pickers import pick_random
10 | from evol.helpers.groups import group_duplicate
11 |
12 |
13 | class RockPaperScissorsPlayer:
14 | arbitrariness = 0.0
15 | elements = ('rock', 'paper', 'scissors')
16 |
17 | def __init__(self, preference=None):
18 | self.preference = preference if preference else choice(self.elements)
19 |
20 | def __repr__(self):
21 | return '{}({})'.format(self.__class__.__name__, self.preference)
22 |
23 | def play(self):
24 | if random() >= self.arbitrariness:
25 | return self.preference
26 | else:
27 | return choice(self.elements)
28 |
29 | def mutate(self, volatility=0.1):
30 | if random() < volatility:
31 | return self.__class__(choice(self.elements))
32 | else:
33 | return self.__class__(self.preference)
34 |
35 | def combine(self, other):
36 | return self.__class__(choice([self.preference, other.preference]))
37 |
38 |
39 | class RockPaperScissorsLizardSpockPlayer(RockPaperScissorsPlayer):
40 | elements = ('rock', 'paper', 'scissors', 'lizard', 'spock')
41 |
42 |
43 | COMBINATIONS = [
44 | ('scissors', 'paper'),
45 | ('paper', 'rock'),
46 | ('rock', 'scissors'),
47 | ('rock', 'lizard'),
48 | ('lizard', 'spock'),
49 | ('spock', 'scissors'),
50 | ('scissors', 'lizard'),
51 | ('lizard', 'paper'),
52 | ('paper', 'spock'),
53 | ('spock', 'rock'),
54 | ]
55 |
56 |
57 | def evaluate(player_1: RockPaperScissorsPlayer, player_2: RockPaperScissorsPlayer) -> List[float]:
58 | choice_1, choice_2 = player_1.play(), player_2.play()
59 | player_choices = {choice_1, choice_2}
60 | if len(player_choices) == 1:
61 | return [0, 0]
62 | for combination in COMBINATIONS:
63 | if set(combination) == player_choices:
64 | return [1, -1] if choice_1 == combination[0] else [-1, 1]
65 |
66 |
67 | class History:
68 |
69 | def __init__(self):
70 | self.history = []
71 |
72 | def log(self, population: ContestPopulation):
73 | preferences = Counter()
74 | for individual in population:
75 | preferences.update([individual.chromosome.preference])
76 | self.history.append(dict(**preferences, id=population.id, generation=population.generation))
77 |
78 | def plot(self):
79 | try:
80 | import pandas as pd
81 | import matplotlib.pylab as plt
82 | df = pd.DataFrame(self.history).set_index(['id', 'generation']).fillna(0)
83 | population_size = sum(df.iloc[0].values)
84 | n_populations = df.reset_index()['id'].nunique()
85 | fig, axes = plt.subplots(nrows=n_populations, figsize=(12, 2*n_populations),
86 | sharex='all', sharey='all', squeeze=False)
87 | for row, (_, pop) in zip(axes, df.groupby('id')):
88 | ax = row[0]
89 | pop.reset_index(level='id', drop=True).plot(ax=ax)
90 | ax.set_ylim([0, population_size])
91 | ax.set_xlabel('iteration')
92 | ax.set_ylabel('# w/ preference')
93 | if n_populations > 1:
94 | for i in range(0, df.reset_index().generation.max(), 50):
95 | ax.axvline(i)
96 | plt.show()
97 | except ImportError:
98 | print("If you install matplotlib and pandas you will get a pretty plot.")
99 |
100 |
101 | def run_rock_paper_scissors(population_size: int = 100,
102 | n_iterations: int = 200,
103 | random_seed: int = 42,
104 | survive_fraction: float = 0.8,
105 | arbitrariness: float = 0.2,
106 | concurrent_workers: int = 1,
107 | lizard_spock: bool = False,
108 | grouped: bool = False,
109 | silent: bool = False):
110 | seed(random_seed)
111 |
112 | RockPaperScissorsPlayer.arbitrariness = arbitrariness
113 |
114 | player_class = RockPaperScissorsLizardSpockPlayer if lizard_spock else RockPaperScissorsPlayer
115 | pop = ContestPopulation(chromosomes=[player_class() for _ in range(population_size)],
116 | eval_function=evaluate, maximize=True,
117 | concurrent_workers=concurrent_workers).evaluate()
118 | history = History()
119 |
120 | evo = Evolution().repeat(
121 | evolution=(Evolution()
122 | .survive(fraction=survive_fraction)
123 | .breed(parent_picker=pick_random, combiner=lambda x, y: x.combine(y), n_parents=2)
124 | .mutate(lambda x: x.mutate())
125 | .evaluate()
126 | .callback(history.log)),
127 | n=n_iterations // 4,
128 | grouping_function=group_duplicate if grouped else None
129 | )
130 |
131 | pop.evolve(evo, n=4)
132 |
133 | if not silent:
134 | history.plot()
135 | return history
136 |
137 |
138 | def parse_arguments():
139 | parser = ArgumentParser(description='Run the rock-paper-scissors example.')
140 | parser.add_argument('--population-size', dest='population_size', type=int, default=100,
141 | help='the number of candidates to start the algorithm with')
142 | parser.add_argument('--n-iterations', dest='n_iterations', type=int, default=200,
143 | help='the number of iterations to run')
144 | parser.add_argument('--random-seed', dest='random_seed', type=int, default=42,
145 | help='the random seed to set')
146 | parser.add_argument('--survive-fraction', dest='survive_fraction', type=float, default=0.8,
147 | help='the fraction of the population to survive each iteration')
148 | parser.add_argument('--arbitrariness', type=float, default=0.2,
149 | help='arbitrariness of the players. if zero, player will always choose its preference')
150 | parser.add_argument('--concurrent_workers', type=int, default=1,
151 | help='Concurrent workers to use to evaluate the population.')
152 | parser.add_argument('--lizard-spock', action='store_true', default=False,
153 | help='Play rock-paper-scissors-lizard-spock.')
154 | parser.add_argument('--grouped', action='store_true', default=False,
155 | help='Run the evolution in four groups.')
156 | return parser.parse_args()
157 |
158 |
159 | if __name__ == "__main__":
160 | args = parse_arguments()
161 | run_rock_paper_scissors(**args.__dict__)
162 |
--------------------------------------------------------------------------------
/examples/simple_callback.py:
--------------------------------------------------------------------------------
1 | """
2 | This example demonstrates how logging works in evolutions.
3 | """
4 |
5 | import random
6 | from evol import Population, Evolution
7 |
8 |
9 | def random_start():
10 | """
11 | This function generates a random (x,y) coordinate in the searchspace
12 | """
13 | return (random.random() - 0.5) * 20, (random.random() - 0.5) * 20
14 |
15 |
16 | def func_to_optimise(xy):
17 | """
18 | This is the function we want to optimise (maximize)
19 | """
20 | x, y = xy
21 | return -(1-x)**2 - 2*(2-x**2)**2
22 |
23 |
24 | def pick_random_parents(pop):
25 | """
26 | This is how we are going to select parents from the population
27 | """
28 | mom = random.choice(pop)
29 | dad = random.choice(pop)
30 | return mom, dad
31 |
32 |
33 | def make_child(mom, dad):
34 | """
35 | This is how two parents are going to make a child.
36 | Note that the output of a tuple, just like the output of `random_start`
37 | """
38 | child_x = (mom[0] + dad[0])/2
39 | child_y = (mom[1] + dad[1])/2
40 | return child_x, child_y
41 |
42 |
43 | def add_noise(chromosome, sigma):
44 | """
45 | This is a function that will add some noise to the chromosome.
46 | """
47 | new_x = chromosome[0] + (random.random()-0.5) * sigma
48 | new_y = chromosome[1] + (random.random()-0.5) * sigma
49 | return new_x, new_y
50 |
51 |
52 | class MyLogger():
53 | def __init__(self):
54 | self.i = 0
55 |
56 | def log(self, pop):
57 | self.i += 1
58 | best = max([i.fitness for i in pop.evaluate()])
59 | print(f"the best score i={self.i} => {best}")
60 |
61 |
62 | if __name__ == "__main__":
63 | logger = MyLogger()
64 | random.seed(42)
65 |
66 | pop = Population(chromosomes=[random_start() for _ in range(200)],
67 | eval_function=func_to_optimise,
68 | maximize=True, concurrent_workers=2)
69 |
70 | evo1 = (Evolution()
71 | .survive(fraction=0.1)
72 | .breed(parent_picker=pick_random_parents, combiner=make_child)
73 | .mutate(mutate_function=add_noise, sigma=0.2)
74 | .evaluate()
75 | .callback(logger.log))
76 |
77 | evo2 = (Evolution()
78 | .survive(n=10)
79 | .breed(parent_picker=pick_random_parents, combiner=make_child)
80 | .mutate(mutate_function=add_noise, sigma=0.1)
81 | .evaluate()
82 | .callback(logger.log))
83 |
84 | evo3 = (Evolution()
85 | .repeat(evo1, n=20)
86 | .repeat(evo2, n=20))
87 |
88 | pop = pop.evolve(evo3, n=3)
89 |
--------------------------------------------------------------------------------
/examples/simple_logging.py:
--------------------------------------------------------------------------------
1 | """
2 | This example demonstrates how logging works in evolutions.
3 | """
4 |
5 | import random
6 | from tempfile import NamedTemporaryFile
7 | from evol import Population, Evolution
8 | from evol.logger import BaseLogger
9 |
10 | random.seed(42)
11 |
12 |
13 | def random_start():
14 | """
15 | This function generates a random (x,y) coordinate in the searchspace
16 | """
17 | return (random.random() - 0.5) * 20, (random.random() - 0.5) * 20
18 |
19 |
20 | def func_to_optimise(xy):
21 | """
22 | This is the function we want to optimise (maximize)
23 | """
24 | x, y = xy
25 | return -(1 - x) ** 2 - 2 * (2 - x ** 2) ** 2
26 |
27 |
28 | def pick_random_parents(pop):
29 | """
30 | This is how we are going to select parents from the population
31 | """
32 | mom = random.choice(pop)
33 | dad = random.choice(pop)
34 | return mom, dad
35 |
36 |
37 | def make_child(mom, dad):
38 | """
39 | This is how two parents are going to make a child.
40 | Note that the output of a tuple, just like the output of `random_start`
41 | """
42 | child_x = (mom[0] + dad[0]) / 2
43 | child_y = (mom[1] + dad[1]) / 2
44 | return child_x, child_y
45 |
46 |
47 | def add_noise(chromosome, sigma):
48 | """
49 | This is a function that will add some noise to the chromosome.
50 | """
51 | new_x = chromosome[0] + (random.random() - 0.5) * sigma
52 | new_y = chromosome[1] + (random.random() - 0.5) * sigma
53 | return new_x, new_y
54 |
55 |
56 | with NamedTemporaryFile() as tmpfile:
57 | logger = BaseLogger(target=tmpfile.name)
58 | pop = Population(chromosomes=[random_start() for _ in range(200)],
59 | eval_function=func_to_optimise,
60 | maximize=True, concurrent_workers=2)
61 |
62 | evo1 = (Evolution()
63 | .survive(fraction=0.1)
64 | .breed(parent_picker=pick_random_parents, combiner=make_child)
65 | .mutate(mutate_function=add_noise, sigma=0.2)
66 | .callback(logger.log))
67 |
68 | evo2 = (Evolution()
69 | .survive(n=10)
70 | .breed(parent_picker=pick_random_parents, combiner=make_child)
71 | .mutate(mutate_function=add_noise, sigma=0.1)
72 | .callback(logger.log))
73 |
74 | evo3 = (Evolution()
75 | .repeat(evo1, n=20)
76 | .repeat(evo2, n=20))
77 |
78 | pop = pop.evolve(evo3, n=3)
79 |
--------------------------------------------------------------------------------
/examples/simple_nonlinear.py:
--------------------------------------------------------------------------------
1 | import random
2 | from random import random as r
3 | from evol import Population, Evolution
4 |
5 | random.seed(42)
6 |
7 |
8 | def random_start():
9 | """
10 | This function generates a random (x,y) coordinate
11 | """
12 | return (r() - 0.5) * 20, (r() - 0.5) * 20
13 |
14 |
15 | def func_to_optimise(xy):
16 | """
17 | This is the function we want to optimise (maximize)
18 | """
19 | x, y = xy
20 | return -(1 - x) ** 2 - (2 - y ** 2) ** 2
21 |
22 |
23 | def pick_random_parents(pop):
24 | """
25 | This is how we are going to select parents from the population
26 | """
27 | mom = random.choice(pop)
28 | dad = random.choice(pop)
29 | return mom, dad
30 |
31 |
32 | def make_child(mom, dad):
33 | """
34 | This function describes how two candidates combine into a
35 | new candidate. Note that the output is a tuple, just like
36 | the output of `random_start`. We leave it to the developer
37 | to ensure that chromosomes are of the same type.
38 | """
39 | child_x = (mom[0] + dad[0]) / 2
40 | child_y = (mom[1] + dad[1]) / 2
41 | return child_x, child_y
42 |
43 |
44 | def add_noise(chromosome, sigma):
45 | """
46 | This is a function that will add some noise to the chromosome.
47 | """
48 | new_x = chromosome[0] + (r() - 0.5) * sigma
49 | new_y = chromosome[1] + (r() - 0.5) * sigma
50 | return new_x, new_y
51 |
52 |
53 | # We start by defining a population with candidates.
54 | pop = Population(chromosomes=[random_start() for _ in range(200)],
55 | eval_function=func_to_optimise, maximize=True)
56 |
57 | # We do a single step here and out comes a new population
58 | pop.survive(fraction=0.5)
59 |
60 | # We do two steps here and out comes a new population
61 | (pop
62 | .survive(fraction=0.5)
63 | .breed(parent_picker=pick_random_parents, combiner=make_child))
64 |
65 | # We do a three steps here and out comes a new population
66 | (pop
67 | .survive(fraction=0.5)
68 | .breed(parent_picker=pick_random_parents, combiner=make_child)
69 | .mutate(mutate_function=add_noise, sigma=1))
70 |
71 | # This is inelegant but it works.
72 | for i in range(5):
73 | pop = (pop
74 | .survive(fraction=0.5)
75 | .breed(parent_picker=pick_random_parents, combiner=make_child)
76 | .mutate(mutate_function=add_noise, sigma=1))
77 |
78 | # We define a sequence of steps to change these candidates
79 | evo1 = (Evolution()
80 | .survive(fraction=0.5)
81 | .breed(parent_picker=pick_random_parents, combiner=make_child)
82 | .mutate(mutate_function=add_noise, sigma=1))
83 |
84 | # We define another sequence of steps to change these candidates
85 | evo2 = (Evolution()
86 | .survive(n=1)
87 | .breed(parent_picker=pick_random_parents, combiner=make_child)
88 | .mutate(mutate_function=add_noise, sigma=0.2))
89 |
90 | # We are combining two evolutions into a third one.
91 | # You don't have to but this approach demonstrates
92 | # the flexibility of the library.
93 | evo3 = (Evolution()
94 | .repeat(evo1, n=50)
95 | .repeat(evo2, n=10)
96 | .evaluate())
97 |
98 | # In this step we are telling evol to apply the evolutions
99 | # to the population of candidates.
100 | pop = pop.evolve(evo3, n=5)
101 | print(f"the best score found: {max([i.fitness for i in pop])}")
102 |
--------------------------------------------------------------------------------
/examples/travelling_salesman.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from argparse import ArgumentParser
3 | from math import sqrt
4 | from random import random, seed, shuffle
5 | from typing import List, Optional
6 |
7 | from evol import Evolution, Population
8 | from evol.helpers.combiners.permutation import cycle_crossover
9 | from evol.helpers.groups import group_stratified
10 | from evol.helpers.mutators.permutation import swap_elements
11 | from evol.helpers.pickers import pick_random
12 |
13 |
14 | def run_travelling_salesman(population_size: int = 100,
15 | n_iterations: int = 10,
16 | random_seed: int = 0,
17 | n_destinations: int = 50,
18 | concurrent_workers: Optional[int] = None,
19 | n_groups: int = 4,
20 | silent: bool = False):
21 | seed(random_seed)
22 | # Generate some destinations
23 | destinations = [(random(), random()) for _ in range(n_destinations)]
24 |
25 | # Given a list of destination indexes, this is our cost function
26 | def evaluate(ordered_destinations: List[int]) -> float:
27 | total = 0
28 | for x, y in zip(ordered_destinations, ordered_destinations[1:]):
29 | coordinates_x = destinations[x]
30 | coordinates_y = destinations[y]
31 | total += sqrt((coordinates_x[0] - coordinates_y[1])**2 + (coordinates_x[1] - coordinates_y[1])**2)
32 | return total
33 |
34 | # This generates a random solution
35 | def generate_solution() -> List[int]:
36 | indexes = list(range(n_destinations))
37 | shuffle(indexes)
38 | return indexes
39 |
40 | def print_function(population: Population):
41 | if population.generation % 5000 == 0 and not silent:
42 | print(f'{population.generation}: {population.documented_best.fitness:1.2f} / '
43 | f'{population.current_best.fitness:1.2f}')
44 |
45 | pop = Population.generate(generate_solution, eval_function=evaluate, maximize=False,
46 | size=population_size * n_groups, concurrent_workers=concurrent_workers)
47 |
48 | island_evo = (Evolution()
49 | .survive(fraction=0.5)
50 | .breed(parent_picker=pick_random, combiner=cycle_crossover)
51 | .mutate(swap_elements, elitist=True))
52 |
53 | evo = (Evolution()
54 | .evaluate(lazy=True)
55 | .callback(print_function)
56 | .repeat(evolution=island_evo, n=100, grouping_function=group_stratified, n_groups=n_groups))
57 |
58 | result = pop.evolve(evolution=evo, n=n_iterations)
59 |
60 | if not silent:
61 | print(f'Shortest route: {result.documented_best.chromosome}')
62 | print(f'Route length: {result.documented_best.fitness}')
63 |
64 |
65 | def parse_arguments():
66 | parser = ArgumentParser(description='Run the travelling salesman example.')
67 | parser.add_argument('--population-size', type=int, default=100,
68 | help='the number of candidates to start the algorithm with')
69 | parser.add_argument('--n-iterations', type=int, default=10,
70 | help='the number of iterations to run')
71 | parser.add_argument('--random-seed', type=int, default=42,
72 | help='the random seed to set')
73 | parser.add_argument('--n-destinations', type=int, default=50,
74 | help='Number of destinations in the route.')
75 | parser.add_argument('--n-groups', type=int, default=4,
76 | help='Number of groups to group by.')
77 | parser.add_argument('--concurrent-workers', type=int, default=None,
78 | help='Concurrent workers to use to evaluate the population.')
79 | return parser.parse_args()
80 |
81 |
82 | if __name__ == "__main__":
83 | args = parse_arguments()
84 | run_travelling_salesman(**args.__dict__)
85 |
--------------------------------------------------------------------------------
/examples/very_basic_tsp.py:
--------------------------------------------------------------------------------
1 | """
2 | There are a few things to notice with this example.
3 |
4 | 1. from the command line you can re-run and see a different matplotlib plot
5 | 2. `n_crossover` is set via the `.breed()` method
6 | 3. the functions you need for this application can be tested with unittests
7 |
8 | """
9 |
10 | import random
11 | import math
12 | import argparse
13 |
14 | from evol import Population, Evolution
15 |
16 |
17 | def run_evolutionary(num_towns=42, population_size=100, num_iter=200, seed=42): # noqa: C901
18 | """
19 | Runs a simple evolutionary algorithm against a simple TSP problem.
20 | The goal is to explain the `evol` library, this is not an algorithm
21 | that should perform well.
22 | """
23 |
24 | # First we generate random towns as candidates with a seed
25 | random.seed(seed)
26 | coordinates = [(random.random(), random.random()) for i in range(num_towns)]
27 |
28 | # Next we define a few functions that we will need in order to create an algorithm.
29 | # Think of these functions as if they are lego blocks.
30 | def init_func(num_towns):
31 | """
32 | This function generates an individual
33 | """
34 | order = list(range(num_towns))
35 | random.shuffle(order)
36 | return order
37 |
38 | def dist(t1, t2):
39 | """
40 | Calculates the distance between two towns.
41 | """
42 | return math.sqrt((t1[0] - t2[0]) ** 2 + (t1[1] - t2[1]) ** 2)
43 |
44 | def eval_func(order):
45 | """
46 | Evaluates a candidate chromosome, which is a list that represents town orders.
47 | """
48 | return sum([dist(coordinates[order[i]], coordinates[order[i - 1]]) for i, t in enumerate(order)])
49 |
50 | def pick_random(parents):
51 | """
52 | This function selects two parents
53 | """
54 | return random.choice(parents), random.choice(parents)
55 |
56 | def partition(lst, n_crossover):
57 | division = len(lst) / n_crossover
58 | return [lst[round(division * i):round(division * (i + 1))] for i in range(n_crossover)]
59 |
60 | def crossover_ox(mom_order, dad_order, n_crossover):
61 | idx_split = partition(range(len(mom_order)), n_crossover=n_crossover)
62 | dad_idx = sum([list(d) for i, d in enumerate(idx_split) if i % 2 == 0], [])
63 | path = [-1 for _ in range(len(mom_order))]
64 | for idx in dad_idx:
65 | path[idx] = dad_order[idx]
66 | cities_visited = {p for p in path if p != -1}
67 | for i, d in enumerate(path):
68 | if d == -1:
69 | city = [p for p in mom_order if p not in cities_visited][0]
70 | path[i] = city
71 | cities_visited.add(city)
72 | return path
73 |
74 | def random_flip(chromosome):
75 | result = chromosome[:]
76 | idx1, idx2 = random.choices(list(range(len(chromosome))), k=2)
77 | result[idx1], result[idx2] = result[idx2], result[idx1]
78 | return result
79 |
80 | pop = Population(chromosomes=[init_func(num_towns) for _ in range(population_size)],
81 | eval_function=eval_func, maximize=False, concurrent_workers=2).evaluate()
82 |
83 | evo = (Evolution()
84 | .survive(fraction=0.1)
85 | .breed(parent_picker=pick_random, combiner=crossover_ox, n_crossover=2)
86 | .mutate(random_flip)
87 | .evaluate())
88 |
89 | print("will start the evolutionary program")
90 | scores = []
91 | iterations = []
92 | for i in range(num_iter):
93 | print(f"iteration: {i} best score: {min([individual.fitness for individual in pop])}")
94 | for indiviual in pop:
95 | scores.append(indiviual.fitness)
96 | iterations.append(i)
97 | pop = pop.evolve(evo)
98 |
99 | try:
100 | import matplotlib.pylab as plt
101 | plt.scatter(iterations, scores, s=1, alpha=0.3)
102 | plt.title("population fitness vs. iteration")
103 | plt.show()
104 | except ImportError:
105 | print("If you install matplotlib you will get a pretty plot.")
106 |
107 |
108 | if __name__ == "__main__":
109 | parser = argparse.ArgumentParser(description='Run an example evol algorithm against a simple TSP problem.')
110 | parser.add_argument('--num_towns', type=int, default=42,
111 | help='the number of towns to generate for the TSP problem')
112 | parser.add_argument('--population_size', type=int, default=100,
113 | help='the number of candidates to start the algorithm with')
114 | parser.add_argument('--num_iter', type=int, default=100,
115 | help='the number of evolutionary cycles to run')
116 | parser.add_argument('--seed', type=int, default=42,
117 | help='the random seed for all this')
118 |
119 | args = parser.parse_args()
120 | print(f"i am aware of these arguments: {args}")
121 | run_evolutionary(num_towns=args.num_towns, population_size=args.population_size,
122 | num_iter=args.num_iter, seed=args.seed)
123 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file=README.md
3 |
4 | [aliases]
5 | test=pytest
6 |
7 | [flake8]
8 | max-complexity=10
9 | max-line-length=120
10 | exclude = */__init__.py, */*/__init__.py
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import codecs
2 | from os import path
3 | from re import search, M
4 | from setuptools import setup, find_packages
5 |
6 |
7 | def load_readme():
8 | this_directory = path.abspath(path.dirname(__file__))
9 | with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
10 | return f.read()
11 |
12 |
13 | here = path.abspath(path.dirname(__file__))
14 |
15 |
16 | def read(*parts):
17 | with codecs.open(path.join(here, *parts), 'r') as fp:
18 | return fp.read()
19 |
20 |
21 | def find_version(*file_paths):
22 | version_file = read(*file_paths)
23 | version_match = search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, M)
24 | if version_match:
25 | return version_match.group(1)
26 | raise RuntimeError("Unable to find version string.")
27 |
28 |
29 | setup(
30 | name='evol',
31 | version=find_version('evol', '__init__.py'),
32 | description='A Grammar for Evolutionary Algorithms and Heuristics',
33 | long_description=load_readme(),
34 | long_description_content_type='text/markdown',
35 | license='MIT License',
36 | author=['Vincent D. Warmerdam', 'Rogier van der Geer'],
37 | author_email='vincentwarmerdam@gmail.com',
38 | url='https://github.com/godatadriven/evol',
39 | packages=find_packages(),
40 | keywords=['genetic', 'algorithms', 'heuristics'],
41 | python_requires='>=3.6',
42 | tests_require=[
43 | "pytest>=3.3.1", "attrs==19.1.0", "flake8>=3.7.9"
44 | ],
45 | extras_require={
46 | "dev": ["pytest>=3.3.1", "attrs==19.1.0", "flake8>=3.7.9"],
47 | "docs": ["sphinx_rtd_theme", "Sphinx>=2.0.0"],
48 | },
49 | setup_requires=[
50 | "pytest-runner"
51 | ],
52 | install_requires=[
53 | "multiprocess>=0.70.6.1"
54 | ],
55 | classifiers=['Intended Audience :: Developers',
56 | 'Intended Audience :: Science/Research',
57 | 'Programming Language :: Python :: 3.6',
58 | 'Development Status :: 3 - Alpha',
59 | 'License :: OSI Approved :: MIT License',
60 | 'Topic :: Scientific/Engineering',
61 | 'Topic :: Scientific/Engineering :: Artificial Intelligence']
62 | )
63 |
--------------------------------------------------------------------------------
/test_local.sh:
--------------------------------------------------------------------------------
1 | flake8 evol
2 | flake8 tests
3 | if [ -x "$(command -v py.test-3)" ]; then
4 | py.test-3
5 | else
6 | python -m pytest
7 | fi
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from random import seed, shuffle
2 |
3 | from pytest import fixture
4 |
5 | from evol import Individual, Population, ContestPopulation, Evolution
6 | from evol.helpers.pickers import pick_random
7 |
8 |
9 | @fixture(scope='module')
10 | def simple_chromosomes():
11 | return list(range(-50, 50))
12 |
13 |
14 | @fixture(scope='module')
15 | def shuffled_chromosomes():
16 | chromosomes = list(range(0, 100)) + list(range(0, 100)) + list(range(0, 100)) + list(range(0, 100))
17 | seed(0)
18 | shuffle(chromosomes)
19 | return chromosomes
20 |
21 |
22 | @fixture(scope='function')
23 | def simple_individuals(simple_chromosomes):
24 | result = [Individual(chromosome=chromosome) for chromosome in simple_chromosomes]
25 | for individual in result:
26 | individual.fitness = 0
27 | return result
28 |
29 |
30 | @fixture(scope='module')
31 | def simple_evaluation_function():
32 | def eval_func(x):
33 | return -x ** 2
34 | return eval_func
35 |
36 |
37 | @fixture(scope='function')
38 | def evaluated_individuals(simple_chromosomes, simple_evaluation_function):
39 | result = [Individual(chromosome=chromosome) for chromosome in simple_chromosomes]
40 | for individual in result:
41 | individual.fitness = individual.chromosome
42 | return result
43 |
44 |
45 | @fixture(scope='module')
46 | def simple_contest_evaluation_function():
47 | def eval_func(x, y, z):
48 | return [1, -1, 0] if x > y else [-1, 1, 0]
49 | return eval_func
50 |
51 |
52 | @fixture(scope='module')
53 | def simple_evolution():
54 | return (
55 | Evolution()
56 | .survive(fraction=0.5)
57 | .breed(parent_picker=pick_random, n_parents=2, combiner=lambda x, y: x + y)
58 | .mutate(lambda x: x + 1, probability=0.1)
59 | )
60 |
61 |
62 | @fixture(scope='function')
63 | def simple_population(simple_chromosomes, simple_evaluation_function):
64 | return Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
65 |
66 |
67 | @fixture(scope='function')
68 | def simple_contestpopulation(simple_chromosomes, simple_contest_evaluation_function):
69 | return ContestPopulation(chromosomes=simple_chromosomes, eval_function=simple_contest_evaluation_function,
70 | contests_per_round=35, individuals_per_contest=3)
71 |
72 |
73 | @fixture(scope='function', params=range(2))
74 | def any_population(request, simple_population, simple_contestpopulation):
75 | if request.param == 0:
76 | return simple_population
77 | elif request.param == 1:
78 | return simple_contestpopulation
79 | else:
80 | raise ValueError("invalid internal test config")
81 |
--------------------------------------------------------------------------------
/tests/helpers/combiners/test_permutation_combiners.py:
--------------------------------------------------------------------------------
1 | from random import seed
2 |
3 | from evol.helpers.combiners.permutation import order_one_crossover, cycle_crossover
4 |
5 |
6 | def test_order_one_crossover_int():
7 | seed(53) # Fix result of select_partition
8 | x, y = (8, 4, 7, 3, 6, 2, 5, 1, 9, 0), (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
9 | a = order_one_crossover(x, y)
10 | assert a == (0, 4, 7, 3, 6, 2, 5, 1, 8, 9)
11 |
12 |
13 | def test_order_one_crossover_str():
14 | seed(53) # Fix result of select_partition
15 | x, y = ('I', 'E', 'H', 'D', 'G', 'C', 'F', 'B', 'J', 'A'), ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J')
16 | a = order_one_crossover(x, y)
17 | assert a == ('A', 'E', 'H', 'D', 'G', 'C', 'F', 'B', 'I', 'J')
18 |
19 |
20 | def test_cycle_crossover_int():
21 | x, y = (8, 4, 7, 3, 6, 2, 5, 1, 9, 0), (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
22 | a, b = cycle_crossover(x, y)
23 | assert a == (8, 1, 2, 3, 4, 5, 6, 7, 9, 0) and b == (0, 4, 7, 3, 6, 2, 5, 1, 8, 9)
24 |
25 |
26 | def test_cycle_crossover_str():
27 | x, y = ('I', 'E', 'H', 'D', 'G', 'C', 'F', 'B', 'J', 'A'), ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J')
28 | a, b = cycle_crossover(x, y)
29 | assert a == ('I', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'A')
30 | assert b == ('A', 'E', 'H', 'D', 'G', 'C', 'F', 'B', 'I', 'J')
31 |
--------------------------------------------------------------------------------
/tests/helpers/mutators/test_permutation_mutators.py:
--------------------------------------------------------------------------------
1 | from random import seed
2 |
3 | from evol.helpers.mutators.permutation import inversion, swap_elements
4 |
5 |
6 | def test_inversion_int():
7 | seed(53) # Fix result of select_partition
8 | x = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
9 | a = inversion(x)
10 | assert a == (0, 1, 2, 7, 6, 5, 4, 3, 8, 9)
11 |
12 |
13 | def test_inversion_str():
14 | seed(53) # Fix result of select_partition
15 | x = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J')
16 | a = inversion(x)
17 | assert a == ('A', 'B', 'C', 'H', 'G', 'F', 'E', 'D', 'I', 'J')
18 |
19 |
20 | def test_swap_elements_int():
21 | seed(0)
22 | x = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
23 | a = swap_elements(x)
24 | assert a == (0, 1, 2, 3, 4, 5, 9, 7, 8, 6)
25 |
26 |
27 | def test_swap_elements_str():
28 | seed(0)
29 | x = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J')
30 | a = swap_elements(x)
31 | assert a == ('A', 'B', 'C', 'D', 'E', 'F', 'J', 'H', 'I', 'G')
32 |
--------------------------------------------------------------------------------
/tests/helpers/test_groups.py:
--------------------------------------------------------------------------------
1 | from random import seed
2 |
3 | from pytest import mark, raises
4 |
5 | from evol import Population
6 | from evol.helpers.groups import group_duplicate, group_stratified, group_random
7 |
8 |
9 | @mark.parametrize('n_groups', [1, 2, 3, 4])
10 | def test_group_duplicate(simple_individuals, n_groups):
11 | indexes = group_duplicate(simple_individuals, n_groups=n_groups)
12 | assert all(len(index) == len(set(index)) for index in indexes)
13 | assert all(len(index) == len(simple_individuals) for index in indexes)
14 | assert len(indexes) == n_groups
15 |
16 |
17 | def test_group_random(simple_individuals):
18 | seed(42)
19 | indexes = group_random(simple_individuals, n_groups=4)
20 | assert sum(len(index) for index in indexes) == len(simple_individuals)
21 | seed(41)
22 | alt_indexes = group_random(simple_individuals, n_groups=4)
23 | assert indexes != alt_indexes
24 |
25 |
26 | class TestGroupStratified:
27 |
28 | def test_unique(self, evaluated_individuals):
29 | indexes = group_stratified(evaluated_individuals, n_groups=2)
30 | assert set(index for island in indexes for index in island) == set(range(len(evaluated_individuals)))
31 |
32 | @mark.parametrize('n_groups', (2, 4))
33 | def test_is_stratified(self, shuffled_chromosomes, n_groups):
34 | population = Population(shuffled_chromosomes, eval_function=lambda x: x).evaluate()
35 | islands = population.group(group_stratified, n_groups=n_groups)
36 | # All islands should have the same total fitness
37 | assert len(set(sum(map(lambda i: i.fitness, island.individuals)) for island in islands)) == 1
38 |
39 | @mark.parametrize('n_groups', (3, 5))
40 | def test_is_nearly_stratified(self, shuffled_chromosomes, n_groups):
41 | population = Population(shuffled_chromosomes, eval_function=lambda x: x).evaluate()
42 | islands = population.group(group_stratified, n_groups=n_groups)
43 | # All islands should have roughly the same total fitness
44 | sum_fitnesses = [sum(map(lambda i: i.fitness, island.individuals)) for island in islands]
45 | assert max(sum_fitnesses) - min(sum_fitnesses) < n_groups * len(islands[0])
46 |
47 | def test_must_be_evaluated(self, simple_population):
48 | with raises(RuntimeError):
49 | simple_population.group(group_stratified)
50 |
--------------------------------------------------------------------------------
/tests/helpers/test_helpers_utils.py:
--------------------------------------------------------------------------------
1 | from evol.helpers.utils import rotating_window, sliding_window
2 |
3 |
4 | def test_sliding_window():
5 | assert list(sliding_window([1, 2, 3, 4])) == [(1, 2), (2, 3), (3, 4)]
6 |
7 |
8 | def test_rotating_window():
9 | assert list(rotating_window([1, 2, 3, 4])) == [(4, 1), (1, 2), (2, 3), (3, 4)]
10 |
--------------------------------------------------------------------------------
/tests/problems/test_functions.py:
--------------------------------------------------------------------------------
1 | from evol.problems.functions import Rosenbrock, Sphere, Rastrigin
2 |
3 |
4 | def test_rosenbrock_optimality():
5 | problem = Rosenbrock(size=2)
6 | assert problem.eval_function((1, 1)) == 0.0
7 | problem = Rosenbrock(size=5)
8 | assert problem.eval_function((1, 1, 1, 1, 1)) == 0.0
9 |
10 |
11 | def test_sphere_optimality():
12 | problem = Sphere(size=2)
13 | assert problem.eval_function((0, 0)) == 0.0
14 | problem = Sphere(size=5)
15 | assert problem.eval_function((0, 0, 0, 0, 0)) == 0.0
16 |
17 |
18 | def test_rastrigin_optimality():
19 | problem = Rastrigin(size=2)
20 | assert problem.eval_function((0, 0)) == 0.0
21 | problem = Rastrigin(size=5)
22 | assert problem.eval_function((0, 0, 0, 0, 0)) == 0.0
23 |
--------------------------------------------------------------------------------
/tests/problems/test_santa.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | import pytest
4 |
5 | from evol.problems.routing import MagicSanta
6 |
7 |
8 | @pytest.fixture
9 | def base_problem():
10 | return MagicSanta(city_coordinates=[(0, 1), (1, 0), (1, 1)],
11 | home_coordinate=(0, 0),
12 | gift_weight=[0, 0, 0])
13 |
14 |
15 | @pytest.fixture
16 | def adv_problem():
17 | return MagicSanta(city_coordinates=[(0, 1), (1, 1), (0, 1)],
18 | home_coordinate=(0, 0),
19 | gift_weight=[5, 1, 1],
20 | sleigh_weight=2)
21 |
22 |
23 | def test_error_raised_wrong_cities(base_problem):
24 | # we want an error if we see too many cities
25 | with pytest.raises(ValueError) as execinfo1:
26 | base_problem.eval_function([[0, 1, 2, 3]])
27 | assert "Extra: {3}" in str(execinfo1.value)
28 | # we want an error if we see too few cities
29 | with pytest.raises(ValueError) as execinfo2:
30 | base_problem.eval_function([[0, 2]])
31 | assert "Missing: {1}" in str(execinfo2.value)
32 | # we want an error if we see multiple occurences of cities
33 | with pytest.raises(ValueError) as execinfo3:
34 | base_problem.eval_function([[0, 2], [0, 1]])
35 | assert "Multiple occurrences found for cities: {0}" in str(execinfo3.value)
36 |
37 |
38 | def test_base_score_method(base_problem):
39 | assert base_problem.distance((0, 0), (0, 2)) == 2
40 | expected = 1 + math.sqrt(2) + 1 + math.sqrt(2)
41 | assert base_problem.eval_function([[0, 1, 2]]) == pytest.approx(expected)
42 | assert base_problem.eval_function([[2, 1, 0]]) == pytest.approx(expected)
43 | base_problem.sleigh_weight = 2
44 | assert base_problem.eval_function([[2, 1, 0]]) == pytest.approx(2*expected)
45 |
46 |
47 | def test_sleight_gift_weights(adv_problem):
48 | expected = (2+7) + (2+2) + (2+1) + (2+0)
49 | assert adv_problem.eval_function([[0, 1, 2]]) == pytest.approx(expected)
50 |
51 |
52 | def test_multiple_routes(adv_problem):
53 | expected = (2 + 6) + (2 + 1) + math.sqrt(2)*(2 + 0) + (2 + 1) + (2 + 0)
54 | assert adv_problem.eval_function([[0, 1], [2]]) == pytest.approx(expected)
55 |
--------------------------------------------------------------------------------
/tests/problems/test_tsp.py:
--------------------------------------------------------------------------------
1 | import math
2 | import pytest
3 | from evol.problems.routing import TSPProblem
4 | from evol.problems.routing.coordinates import united_states_capitols
5 |
6 |
7 | def test_distance_func():
8 | problem = TSPProblem.from_coordinates([(0, 0), (0, 1), (1, 0), (1, 1)])
9 | assert problem.distance_matrix[0][0] == 0
10 | assert problem.distance_matrix[1][0] == 1
11 | assert problem.distance_matrix[0][1] == 1
12 | assert problem.distance_matrix[1][1] == 0
13 | assert problem.distance_matrix[0][2] == 1
14 | assert problem.distance_matrix[0][3] == pytest.approx(math.sqrt(2))
15 |
16 |
17 | def test_score_method():
18 | problem = TSPProblem.from_coordinates([(0, 0), (0, 1), (1, 0), (1, 1)])
19 | expected = 1 + math.sqrt(2) + 1 + math.sqrt(2)
20 | assert problem.eval_function([0, 1, 2, 3]) == pytest.approx(expected)
21 |
22 |
23 | def test_score_method_can_error():
24 | problem = TSPProblem.from_coordinates([(0, 0), (0, 1), (1, 0), (1, 1)])
25 |
26 | with pytest.raises(ValueError) as execinfo1:
27 | problem.eval_function([0, 1, 2, 3, 4])
28 | assert "Solution is longer than number of towns" in str(execinfo1.value)
29 |
30 | with pytest.raises(ValueError) as execinfo2:
31 | problem.eval_function([0, 1, 2])
32 | assert "3" in str(execinfo2.value)
33 | assert "missing" in str(execinfo2.value)
34 |
35 | with pytest.raises(ValueError) as execinfo3:
36 | problem.eval_function([0, 1, 2, 2])
37 | assert "3" in str(execinfo3.value)
38 | assert "missing" in str(execinfo3.value)
39 |
40 |
41 | def test_can_initialize_our_datasets():
42 | problem = TSPProblem.from_coordinates(united_states_capitols)
43 | for i in range(len(united_states_capitols)):
44 | assert problem.distance_matrix[i][i] == 0
45 |
--------------------------------------------------------------------------------
/tests/test_callback.py:
--------------------------------------------------------------------------------
1 | from evol import Population, Evolution
2 | from evol.exceptions import StopEvolution
3 |
4 |
5 | class PopCounter:
6 | def __init__(self):
7 | self.count = 0
8 | self.sum = 0
9 |
10 | def add(self, pop):
11 | for i in pop:
12 | self.count += 1
13 | self.sum += i.chromosome
14 |
15 |
16 | class TestPopulationSimple:
17 | def test_simple_counter_works(self, simple_chromosomes, simple_evaluation_function):
18 | counter = PopCounter()
19 |
20 | pop = Population(chromosomes=simple_chromosomes,
21 | eval_function=simple_evaluation_function)
22 | evo = (Evolution()
23 | .mutate(lambda x: x)
24 | .callback(lambda p: counter.add(p)))
25 |
26 | pop.evolve(evolution=evo, n=1)
27 | assert counter.count == len(simple_chromosomes)
28 | assert counter.sum == sum(simple_chromosomes)
29 | pop.evolve(evolution=evo, n=10)
30 | assert counter.count == len(simple_chromosomes) * 11
31 | assert counter.sum == sum(simple_chromosomes) * 11
32 |
33 | def test_simple_counter_works_every(self, simple_chromosomes, simple_evaluation_function):
34 | counter = PopCounter()
35 |
36 | pop = Population(chromosomes=simple_chromosomes,
37 | eval_function=simple_evaluation_function)
38 | evo = (Evolution()
39 | .mutate(lambda x: x)
40 | .callback(lambda p: counter.add(p), every=2))
41 |
42 | pop.evolve(evolution=evo, n=10)
43 | assert counter.count == len(simple_chromosomes) * 5
44 | assert counter.sum == sum(simple_chromosomes) * 5
45 |
46 | def test_is_evaluated(self, simple_population):
47 | def assert_is_evaluated(pop: Population):
48 | assert pop.current_best is not None
49 |
50 | simple_population.evaluate(lazy=True)
51 | simple_population.callback(assert_is_evaluated)
52 |
53 | def test_stop(self, simple_population, simple_evolution):
54 |
55 | def func(pop):
56 | if pop.generation == 5:
57 | raise StopEvolution
58 |
59 | evo = simple_evolution.callback(func)
60 | assert simple_population.evolve(evo, n=10).generation == 5
61 |
--------------------------------------------------------------------------------
/tests/test_conditions.py:
--------------------------------------------------------------------------------
1 | from time import monotonic, sleep
2 |
3 | from pytest import raises
4 |
5 | from evol import Population
6 | from evol.conditions import Condition, MinimumProgress, TimeLimit
7 | from evol.exceptions import StopEvolution
8 |
9 |
10 | class TestCondition:
11 |
12 | def test_context(self):
13 | with Condition(lambda pop: False):
14 | assert len(Condition.conditions) == 1
15 | with TimeLimit(60):
16 | assert len(Condition.conditions) == 2
17 | assert len(Condition.conditions) == 0
18 |
19 | def test_check(self, simple_population):
20 | with Condition(lambda pop: False):
21 | with raises(StopEvolution):
22 | Condition.check(simple_population)
23 | Condition.check(simple_population)
24 |
25 | def test_evolve(self, simple_population, simple_evolution):
26 | with Condition(lambda pop: pop.generation < 3):
27 | result = simple_population.evolve(simple_evolution, n=5)
28 | assert result.generation == 3
29 |
30 | def test_sequential(self, simple_population, simple_evolution):
31 | with Condition(lambda pop: pop.generation < 3):
32 | result = simple_population.evolve(simple_evolution, n=10)
33 | assert result.generation == 3
34 | with Condition(lambda pop: pop.generation < 6):
35 | result = result.evolve(simple_evolution, n=10)
36 | assert result.generation == 6
37 |
38 |
39 | class TestMinimumProgress:
40 |
41 | def test_evolve(self, simple_evolution):
42 | # The initial population contains the optimal solution.
43 | pop = Population(list(range(100)), eval_function=lambda x: x**2, maximize=False)
44 | with MinimumProgress(window=10):
45 | pop = pop.evolve(simple_evolution, n=100)
46 | assert pop.generation == 10
47 |
48 |
49 | class TestTimeLimit:
50 |
51 | def test_evolve(self, simple_population, simple_evolution):
52 | evo = simple_evolution.callback(lambda p: sleep(1))
53 | start_time = monotonic()
54 | with TimeLimit(seconds=2):
55 | pop = simple_population.evolve(evo, n=10)
56 | assert monotonic() - start_time > 1
57 | assert pop.generation == 2
58 |
--------------------------------------------------------------------------------
/tests/test_evolution.py:
--------------------------------------------------------------------------------
1 | from pytest import mark
2 |
3 | from evol import Evolution, Population
4 | from evol.helpers.groups import group_random, group_duplicate, group_stratified
5 | from evol.helpers.pickers import pick_random
6 |
7 |
8 | class TestEvolution:
9 |
10 | def test_add_step(self):
11 | evo = Evolution()
12 | assert len(evo.chain) == 0
13 | evo_step = evo._add_step('step')
14 | assert len(evo.chain) == 0 # original unchanged
15 | assert evo_step.chain == ['step'] # copy with extra step
16 |
17 | def test_repr(self):
18 | assert repr(Evolution()) == 'Evolution()'
19 | assert repr(Evolution().evaluate()) == 'Evolution(\n EvaluationStep())'
20 | r = 'Evolution(\n RepeatStep() with evolution (10x):\n Evolution(\n' \
21 | ' EvaluationStep()\n SurviveStep()))'
22 | assert repr(Evolution().repeat(Evolution().survive(fraction=0.9), n=10)) == r
23 |
24 |
25 | class TestPopulationEvolve:
26 |
27 | def test_repeat_step(self):
28 | pop = Population([0 for i in range(100)], lambda x: x)
29 | evo = Evolution().repeat(Evolution().survive(fraction=0.9), n=10)
30 | # Check whether an Evolution inside another Evolution is actually applied
31 | assert len(pop.evolve(evo, n=2)) < 50
32 |
33 | @mark.parametrize('n_groups', [2, 4, 5])
34 | @mark.parametrize('grouping_function', [group_stratified, group_duplicate, group_random])
35 | def test_repeat_step_grouped(self, n_groups, grouping_function):
36 | calls = []
37 |
38 | def callback(pop):
39 | calls.append(len(pop))
40 |
41 | sub_evo = (
42 | Evolution()
43 | .survive(fraction=0.5)
44 | .breed(parent_picker=pick_random,
45 | combiner=lambda x, y: x + y)
46 | .callback(callback_function=callback)
47 | )
48 |
49 | pop = Population([0 for _ in range(100)], lambda x: x)
50 | evo = (
51 | Evolution()
52 | .evaluate(lazy=True)
53 | .repeat(sub_evo, grouping_function=grouping_function, n_groups=n_groups)
54 | )
55 | assert len(pop.evolve(evo, n=2)) == 100
56 | assert len(calls) == 2 * n_groups
57 |
--------------------------------------------------------------------------------
/tests/test_examples.py:
--------------------------------------------------------------------------------
1 | from pytest import mark
2 | import sys
3 |
4 | sys.path.append('.')
5 |
6 | from examples.number_of_parents import run_evolutionary # noqa: E402
7 | from examples.rock_paper_scissors import run_rock_paper_scissors # noqa: E402
8 | from examples.travelling_salesman import run_travelling_salesman # noqa: E402
9 |
10 |
11 | N_WORKERS = [1, 2, None]
12 |
13 |
14 | @mark.parametrize('concurrent_workers', N_WORKERS)
15 | def test_number_of_parents(concurrent_workers):
16 | run_evolutionary(verbose=False, workers=concurrent_workers)
17 |
18 |
19 | @mark.parametrize('grouped', (False, True))
20 | def test_rock_paper_scissors(grouped):
21 | history = run_rock_paper_scissors(silent=True, n_iterations=16, grouped=grouped)
22 | assert len(set(h['generation'] for h in history.history)) == 16
23 |
24 |
25 | @mark.skipif(sys.version_info < (3, 7), reason='PyTest cannot deal with the multiprocessing in 3.6.')
26 | @mark.parametrize('n_groups', (1, 4))
27 | def test_travelling_salesman(n_groups):
28 | run_travelling_salesman(concurrent_workers=2, n_groups=n_groups, n_iterations=4, silent=True)
29 |
--------------------------------------------------------------------------------
/tests/test_individual.py:
--------------------------------------------------------------------------------
1 | from copy import copy
2 |
3 | from evol import Individual
4 |
5 |
6 | class TestIndividual:
7 |
8 | def test_init(self):
9 | chromosome = (3, 4)
10 | ind = Individual(chromosome=chromosome)
11 | assert chromosome == ind.chromosome
12 | assert ind.fitness is None
13 |
14 | def test_copy(self):
15 | individual = Individual(chromosome=(1, 2))
16 | individual.evaluate(sum)
17 | copied_individual = copy(individual)
18 | assert individual.chromosome == copied_individual.chromosome
19 | assert individual.fitness == copied_individual.fitness
20 | assert individual.id == copied_individual.id
21 | copied_individual.mutate(lambda x: (2, 3))
22 | assert individual.chromosome == (1, 2)
23 |
24 | def test_evaluate(self):
25 | ind = Individual(chromosome=(1, 2))
26 | ind.evaluate(sum)
27 |
28 | def eval_func1(chromosome):
29 | raise RuntimeError
30 |
31 | ind.evaluate(eval_function=eval_func1, lazy=True)
32 | assert ind.fitness == 3
33 |
34 | def eval_func2(chromosome):
35 | return 5
36 |
37 | ind.evaluate(eval_function=eval_func2, lazy=False)
38 | assert ind.fitness == 5
39 |
40 | def test_mutate(self):
41 | ind = Individual(chromosome=(1, 2, 3))
42 | ind.evaluate(sum)
43 |
44 | def mutate_func(chromosome, value):
45 | return chromosome[0], value, chromosome[2]
46 |
47 | ind.mutate(mutate_func, value=5)
48 | assert (1, 5, 3) == ind.chromosome
49 | assert ind.fitness is None
50 |
--------------------------------------------------------------------------------
/tests/test_logging.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | from evol import Population, Evolution
4 | from evol.helpers.pickers import pick_random
5 | from evol.logger import BaseLogger, SummaryLogger
6 |
7 |
8 | class TestLoggerSimple:
9 |
10 | def test_baselogger_write_file_no_stdout(self, tmpdir, capsys, simple_chromosomes, simple_evaluation_function):
11 | log_file = tmpdir.join('log.txt')
12 | logger = BaseLogger(target=log_file, stdout=False)
13 | pop = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
14 | # we should see that a file was created with an appropriate number of rows
15 | pop.evaluate()
16 | logger.log(pop)
17 | with open(log_file, "r") as f:
18 | assert len(f.readlines()) == len(simple_chromosomes)
19 | # we should see that a file was created with an appropriate number of rows
20 | logger.log(pop)
21 | with open(log_file, "r") as f:
22 | assert len(f.readlines()) == (2 * len(simple_chromosomes))
23 | read_stdout = [line for line in capsys.readouterr().out.split('\n') if line != '']
24 | # there should be nothing printed
25 | assert len(read_stdout) == 0
26 |
27 | def test_baselogger_can_write_to_stdout(self, capsys, simple_chromosomes):
28 | logger = BaseLogger(target=None, stdout=True)
29 |
30 | pop = Population(chromosomes=simple_chromosomes,
31 | eval_function=lambda x: x)
32 | pop.evaluate()
33 | logger.log(pop)
34 | read_stdout = [line for line in capsys.readouterr().out.split('\n') if line != '']
35 | assert len(read_stdout) == len(pop)
36 |
37 | def test_baselogger_can_accept_kwargs(self, tmpdir, simple_chromosomes, simple_evaluation_function):
38 | log_file = tmpdir.join('log.txt')
39 | logger = BaseLogger(target=log_file, stdout=False)
40 | pop = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
41 | pop.evaluate()
42 | # we should see that a file was created with an appropriate number of rows
43 | logger.log(pop, foo="bar")
44 | with open(log_file, "r") as f:
45 | assert len(f.readlines()) == len(simple_chromosomes)
46 | assert all(["bar" in line for line in f.readlines()])
47 | # we should see that a file was created with an appropriate number of rows
48 | logger.log(pop, foo="meh")
49 | with open(log_file, "r") as f:
50 | assert len(f.readlines()) == (2 * len(simple_chromosomes))
51 | assert all(['meh' in line for line in f.readlines()[-10:]])
52 |
53 | def test_baselogger_works_via_evolution_callback(self, tmpdir, capsys):
54 | log_file = tmpdir.join('log.txt')
55 | logger = BaseLogger(target=log_file, stdout=True)
56 | pop = Population(chromosomes=range(10), eval_function=lambda x: x)
57 | evo = (Evolution()
58 | .survive(fraction=0.5)
59 | .breed(parent_picker=pick_random,
60 | combiner=lambda mom, dad: (mom + dad) / 2 + (random.random() - 0.5),
61 | n_parents=2)
62 | .callback(logger.log, foo='bar'))
63 | pop.evolve(evolution=evo, n=2)
64 | # check characteristics of the file
65 | with open(log_file, "r") as f:
66 | read_file = [item.replace("\n", "") for item in f.readlines()]
67 | # size of the log should be appropriate
68 | assert len(read_file) == 2 * len(pop)
69 | # bar needs to be in every single line
70 | assert all(['bar' in row for row in read_file])
71 | # check characteristics of stoud
72 | read_stdout = [line for line in capsys.readouterr().out.split('\n') if line != '']
73 | assert len(read_stdout) == 2 * len(pop)
74 | assert all(['bar' in row for row in read_stdout])
75 |
76 | def test_summarylogger_write_file_mo_stdout(self, tmpdir, capsys):
77 | log_file = tmpdir.join('log.txt')
78 | logger = SummaryLogger(target=log_file, stdout=False)
79 | pop = Population(chromosomes=range(10), eval_function=lambda x: x)
80 | # we should see that a file was created with an appropriate number of rows
81 | pop.evaluate()
82 | logger.log(pop)
83 | with open(log_file, "r") as f:
84 | assert len(f.readlines()) == 1
85 | # we should see that a file was created with an appropriate number of rows
86 | logger.log(pop)
87 | with open(log_file, "r") as f:
88 | assert len(f.readlines()) == 2
89 | read_stdout = [line for line in capsys.readouterr().out.split('\n') if line != '']
90 | # there should be nothing printed
91 | assert len(read_stdout) == 0
92 |
93 | def test_summarylogger_can_write_to_stdout(self, capsys, simple_chromosomes, simple_evaluation_function):
94 | logger = SummaryLogger(target=None, stdout=True)
95 | pop = Population(chromosomes=range(10),
96 | eval_function=lambda x: x)
97 | pop.evaluate()
98 | logger.log(pop)
99 | logger.log(pop)
100 | read_stdout = [line for line in capsys.readouterr().out.split('\n') if line != '']
101 | assert len(read_stdout) == 2
102 |
103 | def test_summary_logger_can_accept_kwargs(self, tmpdir, simple_chromosomes, simple_evaluation_function):
104 | log_file = tmpdir.join('log.txt')
105 | logger = SummaryLogger(target=log_file, stdout=False)
106 | pop = Population(chromosomes=simple_chromosomes,
107 | eval_function=simple_evaluation_function)
108 | pop.evaluate()
109 | # lets make a first simple log
110 | logger.log(pop, foo="bar", buzz="meh")
111 | with open(log_file, "r") as f:
112 | read_lines = f.readlines()
113 | assert len(read_lines) == 1
114 | first_line = read_lines[0]
115 | assert "bar" in first_line
116 | assert "meh" in first_line
117 | last_entry = first_line.split(",")
118 | assert len(last_entry) == 6
119 | # lets log another one
120 | logger.log(pop, buzz="moh")
121 | with open(log_file, "r") as f:
122 | read_lines = f.readlines()
123 | assert len(read_lines) == 2
124 | first_line = read_lines[-1]
125 | assert "moh" in first_line
126 | last_entry = first_line.split(",")
127 | assert len(last_entry) == 5
128 |
129 | def test_summarylogger_works_via_evolution(self, tmpdir, capsys):
130 | log_file = tmpdir.join('log.txt')
131 | logger = SummaryLogger(target=log_file, stdout=True)
132 | pop = Population(chromosomes=list(range(10)), eval_function=lambda x: x)
133 | evo = (Evolution()
134 | .survive(fraction=0.5)
135 | .breed(parent_picker=pick_random,
136 | combiner=lambda mom, dad: (mom + dad) / 2 + (random.random() - 0.5),
137 | n_parents=2)
138 | .evaluate()
139 | .callback(logger.log, foo='bar'))
140 | pop.evolve(evolution=evo, n=5)
141 | # check characteristics of the file
142 | with open(log_file, "r") as f:
143 | read_file = [item.replace("\n", "") for item in f.readlines()]
144 | # size of the log should be appropriate
145 | assert len(read_file) == 5
146 | # bar needs to be in every single line
147 | assert all(['bar' in row for row in read_file])
148 | # check characteristics of stoud
149 | read_stdout = [line for line in capsys.readouterr().out.split('\n') if line != '']
150 | assert len(read_stdout) == 5
151 | assert all(['bar' in row for row in read_stdout])
152 |
153 | def test_two_populations_can_use_same_logger(self, tmpdir, capsys):
154 | log_file = tmpdir.join('log.txt')
155 | logger = SummaryLogger(target=log_file, stdout=True)
156 | pop1 = Population(chromosomes=list(range(10)), eval_function=lambda x: x)
157 | pop2 = Population(chromosomes=list(range(10)), eval_function=lambda x: x)
158 | evo = (Evolution()
159 | .survive(fraction=0.5)
160 | .breed(parent_picker=pick_random,
161 | combiner=lambda mom, dad: (mom + dad) + 1,
162 | n_parents=2)
163 | .evaluate()
164 | .callback(logger.log, foo="dino"))
165 | pop1.evolve(evolution=evo, n=5)
166 | pop2.evolve(evolution=evo, n=5)
167 | # two evolutions have now been applied, lets check the output!
168 | with open(log_file, "r") as f:
169 | read_file = [item.replace("\n", "") for item in f.readlines()]
170 | # print(read_file)
171 | # size of the log should be appropriate
172 | assert len(read_file) == 10
173 | # dino needs to be in every single line
174 | assert all(['dino' in row for row in read_file])
175 | # check characteristics of stoud
176 | read_stdout = [line for line in capsys.readouterr().out.split('\n') if line != '']
177 | assert len(read_stdout) == 10
178 | assert all(['dino' in row for row in read_stdout])
179 |
180 | def test_every_mechanic_in_evolution_log(self, tmpdir, capsys):
181 | log_file = tmpdir.join('log.txt')
182 | logger = SummaryLogger(target=log_file, stdout=True)
183 | pop = Population(chromosomes=list(range(10)), eval_function=lambda x: x)
184 | evo = (Evolution()
185 | .survive(fraction=0.5)
186 | .breed(parent_picker=pick_random,
187 | combiner=lambda mom, dad: (mom + dad) + 1,
188 | n_parents=2)
189 | .evaluate()
190 | .callback(logger.log, every=2))
191 | pop.evolve(evolution=evo, n=100)
192 | with open(log_file, "r") as f:
193 | read_file = [item.replace("\n", "") for item in f.readlines()]
194 | assert len(read_file) == 50
195 | # check characteristics of stoud
196 | read_stdout = [line for line in capsys.readouterr().out.split('\n') if line != '']
197 | assert len(read_stdout) == 50
198 |
--------------------------------------------------------------------------------
/tests/test_parallel_population.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godatadriven/evol/1274b675b2ca3c48a388fa9b167d9c140a54e6dd/tests/test_parallel_population.py
--------------------------------------------------------------------------------
/tests/test_population.py:
--------------------------------------------------------------------------------
1 | from time import sleep, time
2 |
3 | import os
4 | from copy import copy
5 | from pytest import raises, mark
6 | from random import random, choices, seed
7 |
8 | from evol import Population, ContestPopulation
9 | from evol.helpers.groups import group_duplicate, group_stratified
10 | from evol.helpers.pickers import pick_random
11 | from evol.population import Contest
12 |
13 |
14 | class TestPopulationSimple:
15 |
16 | def test_filter_works(self, simple_chromosomes, simple_evaluation_function):
17 | pop = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
18 | assert len(pop.filter(func=lambda i: random() > 0.5)) < 200
19 |
20 | def test_population_init(self, simple_chromosomes):
21 | pop = Population(simple_chromosomes, eval_function=lambda x: x)
22 | assert len(pop) == len(simple_chromosomes)
23 | assert pop.intended_size == len(pop)
24 |
25 | def test_population_generate(self, simple_evaluation_function):
26 | def init_func():
27 | return 1
28 |
29 | pop = Population.generate(init_function=init_func, eval_function=simple_evaluation_function, size=200)
30 | assert len(pop) == 200
31 | assert pop.intended_size == 200
32 | assert pop.individuals[0].chromosome == 1
33 |
34 | def test_is_evaluated(self, any_population):
35 | assert not any_population.is_evaluated
36 | assert any_population.evaluate().is_evaluated
37 |
38 |
39 | class TestPopulationCopy:
40 |
41 | def test_population_copy(self, any_population):
42 | copied_population = copy(any_population)
43 | for key in any_population.__dict__.keys():
44 | if key not in ('id', 'individuals'):
45 | assert copied_population.__dict__[key] == any_population.__dict__[key]
46 |
47 | def test_population_is_evaluated(self, any_population):
48 | evaluated_population = any_population.evaluate()
49 | copied_population = copy(evaluated_population)
50 | assert evaluated_population.is_evaluated
51 | assert copied_population.is_evaluated
52 |
53 |
54 | class TestPopulationEvaluate:
55 |
56 | cpus = os.cpu_count()
57 | latency = 0.005
58 |
59 | def test_individuals_are_not_initially_evaluated(self, any_population):
60 | assert all([i.fitness is None for i in any_population])
61 |
62 | def test_evaluate_lambda(self, simple_chromosomes):
63 | # without concurrency (note that I'm abusing a boolean operator to introduce some latency)
64 | pop = Population(simple_chromosomes, eval_function=lambda x: (sleep(self.latency) or x))
65 | t0 = time()
66 | pop.evaluate()
67 | t1 = time()
68 | single_proc_time = t1 - t0
69 | for individual in pop:
70 | assert individual.chromosome == individual.fitness
71 | # with concurrency
72 | pop = Population(simple_chromosomes, eval_function=lambda x: (sleep(self.latency) or x),
73 | concurrent_workers=self.cpus)
74 | t0 = time()
75 | pop.evaluate()
76 | t1 = time()
77 | multi_proc_time = t1 - t0
78 | for individual in pop:
79 | assert individual.chromosome == individual.fitness
80 | if self.cpus > 1:
81 | assert multi_proc_time < single_proc_time
82 |
83 | def test_evaluate_func(self, simple_chromosomes):
84 | def evaluation_function(x):
85 | sleep(self.latency)
86 | return x * x
87 | pop = Population(simple_chromosomes, eval_function=evaluation_function)
88 | t0 = time()
89 | pop.evaluate()
90 | t1 = time()
91 | single_proc_time = t1 - t0
92 | for individual in pop:
93 | assert evaluation_function(individual.chromosome) == individual.fitness
94 | # with concurrency
95 | pop = Population(simple_chromosomes, eval_function=evaluation_function, concurrent_workers=self.cpus)
96 | t0 = time()
97 | pop.evaluate()
98 | t1 = time()
99 | multi_proc_time = t1 - t0
100 | for individual in pop:
101 | assert evaluation_function(individual.chromosome) == individual.fitness
102 | if self.cpus > 1:
103 | assert multi_proc_time < single_proc_time
104 |
105 | def test_evaluate_lazy(self, any_population):
106 | pop = any_population
107 | pop.evaluate(lazy=True) # should evaluate
108 |
109 | def raise_function(_):
110 | raise Exception
111 |
112 | pop.eval_function = raise_function
113 | pop.evaluate(lazy=True) # should not evaluate
114 | with raises(Exception):
115 | pop.evaluate(lazy=False)
116 |
117 |
118 | class TestPopulationSurvive:
119 |
120 | def test_survive_n_works(self, simple_chromosomes, simple_evaluation_function):
121 | pop1 = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
122 | pop2 = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
123 | pop3 = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
124 | assert len(pop1) == len(simple_chromosomes)
125 | assert len(pop2.survive(n=50)) == 50
126 | assert len(pop3.survive(n=75, luck=True)) == 75
127 |
128 | def test_survive_p_works(self, simple_chromosomes, simple_evaluation_function):
129 | pop1 = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
130 | pop2 = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
131 | pop3 = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
132 | assert len(pop1) == len(simple_chromosomes)
133 | assert len(pop2.survive(fraction=0.5)) == len(simple_chromosomes) * 0.5
134 | assert len(pop3.survive(fraction=0.1, luck=True)) == len(simple_chromosomes) * 0.1
135 |
136 | def test_survive_n_and_p_works(self, simple_evaluation_function):
137 | chromosomes = list(range(200))
138 | pop1 = Population(chromosomes=chromosomes, eval_function=simple_evaluation_function)
139 | pop2 = Population(chromosomes=chromosomes, eval_function=simple_evaluation_function)
140 | pop3 = Population(chromosomes=chromosomes, eval_function=simple_evaluation_function)
141 | assert len(pop1.survive(fraction=0.5, n=200)) == 100
142 | assert len(pop2.survive(fraction=0.9, n=10)) == 10
143 | assert len(pop3.survive(fraction=0.5, n=190, luck=True)) == 100
144 |
145 | def test_breed_increases_generation(self, any_population):
146 | assert any_population.breed(parent_picker=pick_random, combiner=lambda mom, dad: mom).generation == 1
147 |
148 | def test_survive_throws_correct_errors(self, any_population):
149 | """If the resulting population is zero or larger than initial we need to see errors."""
150 | with raises(RuntimeError):
151 | any_population.survive(n=0)
152 | with raises(ValueError):
153 | any_population.survive(n=250)
154 | with raises(ValueError):
155 | any_population.survive()
156 |
157 |
158 | class TestPopulationBreed:
159 |
160 | def test_breed_amount_works(self, simple_chromosomes, simple_evaluation_function):
161 | pop1 = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
162 | pop1.survive(n=50).breed(parent_picker=lambda population: choices(population, k=2),
163 | combiner=lambda mom, dad: (mom + dad) / 2)
164 | assert len(pop1) == len(simple_chromosomes)
165 | pop2 = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
166 | pop2.survive(n=50).breed(parent_picker=lambda population: choices(population, k=2),
167 | combiner=lambda mom, dad: (mom + dad) / 2, population_size=400)
168 | assert len(pop2) == 400
169 | assert pop2.intended_size == 400
170 | assert pop1.generation == 1
171 | assert pop2.generation == 1
172 |
173 | def test_breed_works_with_kwargs(self, simple_chromosomes, simple_evaluation_function):
174 | pop1 = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
175 | pop1.survive(n=50).breed(parent_picker=pick_random,
176 | combiner=lambda mom, dad: (mom + dad) / 2,
177 | n_parents=2)
178 | assert len(pop1) == len(simple_chromosomes)
179 | pop2 = Population(chromosomes=simple_chromosomes, eval_function=simple_evaluation_function)
180 | pop2.survive(n=50).breed(parent_picker=pick_random,
181 | combiner=lambda *parents: sum(parents)/len(parents),
182 | population_size=400, n_parents=3)
183 | assert len(pop2) == 400
184 | assert pop2.intended_size == 400
185 |
186 | def test_breed_raises_with_multiple_values_for_kwarg(self, simple_population):
187 |
188 | (simple_population
189 | .survive(fraction=0.5)
190 | .breed(parent_picker=pick_random,
191 | combiner=lambda x, y: x + y))
192 |
193 | with raises(TypeError):
194 | (simple_population
195 | .survive(fraction=0.5)
196 | .breed(parent_picker=pick_random,
197 | combiner=lambda x, y: x + y, y=2))
198 |
199 |
200 | class TestPopulationMutate:
201 |
202 | def test_mutate_lambda(self):
203 | pop = Population([1]*100, eval_function=lambda x: x).mutate(lambda x: x+1)
204 | for chromosome in pop.chromosomes:
205 | assert chromosome == 2
206 | assert len(pop) == 100
207 |
208 | def test_mutate_inplace(self):
209 | pop = Population([1]*100, eval_function=lambda x: x)
210 | pop.mutate(lambda x: x+1)
211 | for chromosome in pop.chromosomes:
212 | assert chromosome == 2
213 |
214 | def test_mutate_func(self):
215 | def mutate_func(x):
216 | return -x
217 | population = Population([1]*100, eval_function=lambda x: x)
218 | population.mutate(mutate_func)
219 | for chromosome in population.chromosomes:
220 | assert chromosome == -1
221 | assert len(population) == 100
222 |
223 | def test_mutate_probability(self):
224 | seed(0)
225 | pop = Population([1]*100, eval_function=lambda x: x).mutate(lambda x: x+1, probability=0.5).evaluate()
226 | assert min(individual.chromosome for individual in pop.individuals) == 1
227 | assert max(individual.chromosome for individual in pop.individuals) == 2
228 | assert pop.current_best.fitness == 2
229 | assert pop.documented_best.fitness == 2
230 | assert len(pop) == 100
231 |
232 | def test_mutate_zero_probability(self):
233 | pop = Population([1]*100, eval_function=lambda x: x).mutate(lambda x: x+1, probability=0)
234 | for chromosome in pop.chromosomes:
235 | assert chromosome == 1
236 |
237 | def test_mutate_func_kwargs(self):
238 | def mutate_func(x, y=0):
239 | return x+y
240 | pop = Population([1]*100, eval_function=lambda x: x).mutate(mutate_func, y=16)
241 | for chromosome in pop.chromosomes:
242 | assert chromosome == 17
243 |
244 | def test_mutate_elitist(self):
245 | pop = Population([1, 1, 3], eval_function=lambda x: x).evaluate().mutate(lambda x: x + 1, elitist=True)
246 | for chromosome in pop.chromosomes:
247 | assert 1 < chromosome <= 3
248 | assert len(pop) == 3
249 |
250 |
251 | class TestPopulationWeights:
252 |
253 | def test_weights(self, simple_chromosomes, simple_evaluation_function):
254 | for maximize in (False, True):
255 | pop = Population(chromosomes=simple_chromosomes,
256 | eval_function=simple_evaluation_function, maximize=maximize)
257 | with raises(RuntimeError):
258 | assert min(pop._individual_weights) >= 0
259 | pop.evaluate()
260 | assert max(pop._individual_weights) == 1
261 | assert min(pop._individual_weights) == 0
262 | if maximize:
263 | assert pop._individual_weights[0] == 0
264 | else:
265 | assert pop._individual_weights[0] == 1
266 |
267 |
268 | class TestPopulationBest:
269 |
270 | def test_current_best(self, simple_chromosomes):
271 | for maximize, best in ((True, max(simple_chromosomes)), (False, min(simple_chromosomes))):
272 | pop = Population(chromosomes=simple_chromosomes, eval_function=float, maximize=maximize)
273 | assert pop.current_best is None
274 | pop.evaluate()
275 | assert pop.current_best.chromosome == best
276 |
277 | def test_current_worst(self, simple_chromosomes):
278 | for maximize, worst in ((False, max(simple_chromosomes)), (True, min(simple_chromosomes))):
279 | pop = Population(chromosomes=simple_chromosomes, eval_function=float, maximize=maximize)
280 | assert pop.current_worst is None
281 | pop.evaluate()
282 | assert pop.current_worst.chromosome == worst
283 |
284 | def test_mutate_resets(self):
285 | pop = Population(chromosomes=[1, 1, 1], eval_function=float, maximize=True)
286 | assert pop.current_best is None and pop.current_worst is None
287 | pop.evaluate()
288 | assert pop.current_best.fitness == 1 and pop.current_worst.fitness == 1
289 | pop.mutate(lambda x: x)
290 | assert pop.current_best is None and pop.current_worst is None
291 |
292 | def test_documented_best(self):
293 | pop = Population(chromosomes=[100, 100, 100], eval_function=lambda x: x*2, maximize=True)
294 | assert pop.documented_best is None
295 | pop.evaluate()
296 | assert pop.documented_best.fitness == pop.current_best.fitness
297 | pop.mutate(mutate_function=lambda x: x - 10, probability=1).evaluate()
298 | assert pop.documented_best.fitness - 20 == pop.current_best.fitness
299 |
300 |
301 | class TestPopulationIslands:
302 |
303 | @mark.parametrize('n_groups', [1, 2, 3, 4])
304 | def test_groups(self, simple_population, n_groups):
305 | groups = simple_population.group(group_duplicate, n_groups=n_groups)
306 | assert len(groups) == n_groups
307 | assert type(groups) == list
308 | assert all(type(group) is Population for group in groups)
309 |
310 | def test_no_groups(self, simple_population):
311 | with raises(ValueError):
312 | simple_population.group(group_duplicate, n_groups=0)
313 |
314 | def test_empty_group(self, simple_population):
315 | def rogue_grouping_function(*args):
316 | return [[1, 2, 3], []]
317 |
318 | with raises(ValueError):
319 | simple_population.group(rogue_grouping_function)
320 |
321 | @mark.parametrize('result, error', [
322 | (['a', 'b', 'c'], TypeError),
323 | ([None, None], TypeError),
324 | ([10, 100, 1000], IndexError)
325 | ])
326 | def test_invalid_group(self, simple_population, result, error):
327 | def rogue_grouping_function(*args):
328 | return [result]
329 |
330 | with raises(error):
331 | simple_population.group(rogue_grouping_function)
332 |
333 | def test_not_evaluated(self, simple_population):
334 | with raises(RuntimeError):
335 | simple_population.group(group_stratified, n_groups=3)
336 |
337 | def test_combine(self, simple_population):
338 | groups = simple_population.evaluate().group(group_stratified, n_groups=3)
339 | combined = Population.combine(*groups)
340 | assert combined.intended_size == simple_population.intended_size
341 |
342 | def test_combine_nothing(self):
343 | with raises(ValueError):
344 | Population.combine()
345 |
346 |
347 | class TestContest:
348 |
349 | def test_assign_score(self, simple_individuals):
350 | contest = Contest(simple_individuals)
351 | contest.assign_scores(range(len(simple_individuals)))
352 | for score, individual in zip(range(len(simple_individuals)), simple_individuals):
353 | assert individual.fitness == score
354 |
355 | @mark.parametrize('individuals_per_contest,contests_per_round', [(2, 1), (5, 1), (7, 1), (2, 5), (5, 4), (3, 3)])
356 | def test_generate_n_contests(self, simple_individuals, individuals_per_contest, contests_per_round):
357 | contests = Contest.generate(simple_individuals, contests_per_round=contests_per_round,
358 | individuals_per_contest=individuals_per_contest)
359 | for contest in contests:
360 | contest.assign_scores([1]*individuals_per_contest) # Now the fitness equals the number of contests played
361 | # All individuals competed in the same number of contests
362 | assert len({individual.fitness for individual in simple_individuals}) == 1
363 | # The number of contests is _at least_ contests_per_round
364 | assert all([individual.fitness >= contests_per_round for individual in simple_individuals])
365 | # The number of contests is smaller than contests_per_round + individuals_per_contest
366 | assert all([individual.fitness < contests_per_round + individuals_per_contest
367 | for individual in simple_individuals])
368 |
369 |
370 | class TestContestPopulation:
371 |
372 | def test_init(self):
373 | cp = ContestPopulation([0, 1, 2], lambda x: x, contests_per_round=15, individuals_per_contest=15)
374 | assert cp.contests_per_round == 15
375 | assert cp.individuals_per_contest == 15
376 |
377 |
378 | class TestContestPopulationBest:
379 |
380 | def test_no_documented(self):
381 | pop = ContestPopulation([0, 1, 2], lambda x, y: [0, 0], contests_per_round=100, individuals_per_contest=2)
382 | pop.evaluate()
383 | assert pop.documented_best is None
384 | # with concurrency
385 | pop = ContestPopulation([0, 1, 2], lambda x, y: [0, 0], contests_per_round=100, individuals_per_contest=2,
386 | concurrent_workers=3)
387 | pop.evaluate()
388 | assert pop.documented_best is None
389 | pop = ContestPopulation([0, 1, 2],
390 | lambda x, y: [x, y],
391 | contests_per_round=100, individuals_per_contest=2)
392 | pop.evaluate()
393 | assert pop.documented_best is None
394 | # with concurrency
395 | pop = ContestPopulation([0, 1, 2],
396 | lambda x, y: [x, y],
397 | contests_per_round=100, individuals_per_contest=2,
398 | concurrent_workers=3)
399 | pop.evaluate()
400 | assert pop.documented_best is None
401 |
--------------------------------------------------------------------------------
/tests/test_serialization.py:
--------------------------------------------------------------------------------
1 | from os import listdir
2 | from pytest import raises
3 |
4 | from evol import Population, Evolution
5 | from evol.serialization import SimpleSerializer
6 |
7 |
8 | class TestPickleCheckpoint:
9 | method = 'pickle'
10 | extension = '.pkl'
11 | exception = AttributeError
12 |
13 | def test_checkpoint(self, tmpdir, simple_population):
14 | directory = tmpdir.mkdir("ckpt")
15 | simple_population.checkpoint(target=directory, method=self.method)
16 | assert len(listdir(directory)) == 1
17 | assert listdir(directory)[0].endswith(self.extension)
18 |
19 | def test_unserializable_chromosome(self, tmpdir):
20 | directory = tmpdir.mkdir("ckpt")
21 |
22 | class UnSerializableChromosome:
23 | def __init__(self, x):
24 | self.x = x
25 |
26 | pop = Population([UnSerializableChromosome(i) for i in range(10)], lambda x: x.x)
27 | with raises(self.exception):
28 | pop.checkpoint(target=directory, method=self.method)
29 |
30 | def test_load(self, tmpdir, simple_population):
31 | directory = tmpdir.mkdir("ckpt")
32 | simple_population.checkpoint(target=directory, method=self.method)
33 | pop = Population.load(directory, lambda x: x['x'])
34 | assert len(simple_population) == len(pop)
35 | assert all(x.__dict__ == y.__dict__ for x, y in zip(simple_population, pop))
36 |
37 | def test_load_invalid_target(self, tmpdir):
38 | directory = tmpdir.mkdir('ckpt')
39 | with raises(FileNotFoundError):
40 | Population.load(directory.join('no_file' + self.extension), lambda x: x)
41 | with raises(FileNotFoundError):
42 | Population.load(directory, lambda x: x)
43 | txt_file = directory.join('file.txt')
44 | txt_file.write('Something')
45 | with raises(ValueError):
46 | Population.load(txt_file.strpath, lambda x: x)
47 |
48 | def test_checkpoint_invalid_target(self, tmpdir, simple_population):
49 | directory = tmpdir.mkdir("ckpt")
50 | with raises(ValueError):
51 | simple_population.checkpoint(target=None, method=self.method)
52 | txt_file = directory.join('file.txt')
53 | txt_file.write('Something')
54 | with raises(FileNotFoundError):
55 | simple_population.checkpoint(target=txt_file, method=self.method)
56 | # FileExistsError is difficult to test due to timing
57 |
58 | def test_override_default_path(self, tmpdir, simple_chromosomes, simple_evaluation_function):
59 | # With checkpoint_target init
60 | directory1 = tmpdir.mkdir("ckpt1")
61 | pop1 = Population(simple_chromosomes, simple_evaluation_function, checkpoint_target=directory1)
62 | pop1.checkpoint(method=self.method)
63 | assert len(listdir(directory1)) == 1
64 | # With serializer init
65 | directory2 = tmpdir.mkdir("ckpt2")
66 | pop2 = Population(simple_chromosomes, simple_evaluation_function,
67 | serializer=SimpleSerializer(target=directory2))
68 | pop2.checkpoint(method=self.method)
69 | assert len(listdir(directory2)) == 1
70 | # With override
71 | directory3 = tmpdir.mkdir("ckpt3")
72 | pop1.checkpoint(target=directory3, method=self.method)
73 | pop2.checkpoint(target=directory3, method=self.method)
74 | assert len(listdir(directory3)) == 2
75 |
76 | def test_evolution(self, tmpdir, simple_population):
77 | directory = tmpdir.mkdir("ckpt")
78 | evo = Evolution().mutate(lambda x: x+1).checkpoint(target=directory, method=self.method, every=1)
79 | simple_population.evolve(evolution=evo, n=100)
80 | assert len(listdir(directory)) == 100
81 |
82 | def test_every(self, tmpdir, simple_population):
83 | directory = tmpdir.mkdir('ckpt')
84 | evo = Evolution().mutate(lambda x: x+1).checkpoint(target=directory, method=self.method, every=10)
85 | simple_population.evolve(evolution=evo, n=9)
86 | assert len(listdir(directory)) == 0
87 | simple_population.evolve(evolution=evo, n=11)
88 | assert len(listdir(directory)) == 2
89 |
90 |
91 | class TestJsonCheckpoint(TestPickleCheckpoint):
92 | method = 'json'
93 | extension = '.json'
94 | exception = TypeError
95 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from pytest import mark
2 |
3 | from evol import Population, Individual
4 | from evol.helpers.pickers import pick_random
5 | from evol.utils import offspring_generator, select_arguments
6 |
7 |
8 | class TestOffspringGenerator:
9 |
10 | def test_simple_combiner(self, simple_population: Population):
11 | def combiner(x, y):
12 | return 1
13 |
14 | result = offspring_generator(parents=simple_population.individuals,
15 | parent_picker=pick_random, combiner=combiner)
16 | assert isinstance(next(result), Individual)
17 | assert next(result).chromosome == 1
18 |
19 | @mark.parametrize('n_parents', [1, 2, 3, 4])
20 | def test_args(self, n_parents: int, simple_population: Population):
21 | def combiner(*parents, n_parents):
22 | assert len(parents) == n_parents
23 | return 1
24 |
25 | result = offspring_generator(parents=simple_population.individuals, n_parents=n_parents,
26 | parent_picker=pick_random, combiner=combiner)
27 | assert isinstance(next(result), Individual)
28 | assert next(result).chromosome == 1
29 |
30 | def test_simple_picker(self, simple_population: Population):
31 | def combiner(x):
32 | return 1
33 |
34 | def picker(parents):
35 | return parents[0]
36 |
37 | result = offspring_generator(parents=simple_population.individuals, parent_picker=picker, combiner=combiner)
38 | assert isinstance(next(result), Individual)
39 | assert next(result).chromosome == 1
40 |
41 | def test_multiple_offspring(self, simple_population: Population):
42 | def combiner(x, y):
43 | yield 1
44 | yield 2
45 |
46 | result = offspring_generator(parents=simple_population.individuals,
47 | parent_picker=pick_random, combiner=combiner)
48 | for _ in range(10):
49 | assert next(result).chromosome == 1
50 | assert next(result).chromosome == 2
51 |
52 |
53 | class TestSelectArguments:
54 |
55 | @mark.parametrize('args,kwargs,result', [((1, ), {}, 1), ((1, 2), {'x': 1}, 3), ((4, 5), {'z': 8}, 9)])
56 | def test_no_kwargs(self, args, kwargs, result):
57 | @select_arguments
58 | def fct(*args):
59 | return sum(args)
60 | assert fct(*args, **kwargs) == result
61 |
62 | @mark.parametrize('args,kwargs,result', [((1, ), {}, 1), ((1, 2), {'x': 1}, 3), ((4, 5), {'z': 8}, 17)])
63 | def test_with_kwargs(self, args, kwargs, result):
64 | @select_arguments
65 | def fct(*args, z=0):
66 | return sum(args)+z
67 | assert fct(*args, **kwargs) == result
68 |
69 | @mark.parametrize('args,kwargs,result', [((1,), {'b': 3}, 4), ((1, 2), {'x': 1}, 4), ((4, 5), {'z': 8}, 17)])
70 | def test_all_kwargs(self, args, kwargs, result):
71 | @select_arguments
72 | def fct(a, b=0, **kwargs):
73 | return a + b + sum(kwargs.values())
74 | assert fct(*args, **kwargs) == result
75 |
--------------------------------------------------------------------------------