├── .gitattributes ├── .github └── workflows │ ├── deploy.yml │ └── testing.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── Theory │ ├── DE.ipynb │ ├── Rank-and-Crowding.ipynb │ ├── Survival.ipynb │ └── plots.py │ ├── Usage │ ├── Complete-tutorial.ipynb │ ├── Many-objective.ipynb │ ├── Multi-objective.ipynb │ ├── Portfolio.ipynb │ ├── Single-objective.ipynb │ └── index.ipynb │ ├── algorithms.rst │ ├── conf.py │ ├── index.rst │ ├── modules.rst │ ├── pymoode.algorithms.rst │ ├── pymoode.cython.rst │ ├── pymoode.misc.rst │ ├── pymoode.operators.rst │ ├── pymoode.performance.rst │ ├── pymoode.rst │ ├── pymoode.survival.rst │ ├── references.bib │ └── references.rst ├── images ├── gde3mnn_example.gif ├── gde3mnn_example.png ├── general_de.png ├── logo_header_pymoode.png ├── logo_pymoode.png ├── moo_gde3_f_video.gif ├── rastrigin.gif ├── soo_video.gif └── tnk_gde3.png ├── notebooks ├── Many-objective.ipynb ├── Multi-objective.ipynb ├── Portfolio.ipynb ├── Single-objective.ipynb ├── example_readme.ipynb └── tutorial.ipynb ├── pymoode ├── __init__.py ├── algorithms │ ├── __init__.py │ ├── base │ │ ├── __init__.py │ │ ├── differential.py │ │ ├── evolutionary.py │ │ └── genetic.py │ ├── de.py │ ├── gde3.py │ ├── nsde.py │ └── nsder.py ├── backups │ ├── mnn_arrays.pyx │ ├── mnn_bak.pyx │ ├── mnn_bak2.pyx │ ├── mnn_bounded.pyx │ ├── mnn_condensed.pyx │ ├── mnn_naive_sort.pyx │ └── mnn_precalc.pyx ├── cython │ ├── __init__.py │ ├── info.cpp │ ├── info.pyx │ ├── mnn.cpp │ ├── mnn.pyx │ ├── pruning_cd.cpp │ ├── pruning_cd.pyx │ ├── spacing_neighbors.cpp │ ├── spacing_neighbors.pyx │ └── utils.pxd ├── misc │ ├── __init__.py │ ├── mnn.py │ └── pruning_cd.py ├── operators │ ├── __init__.py │ ├── dem.py │ ├── deop.py │ ├── des.py │ ├── dex.py │ └── variant.py ├── performance │ ├── __init__.py │ └── _spacing.py └── survival │ ├── __init__.py │ ├── fitness.py │ ├── rank_and_crowding │ ├── __init__.py │ ├── metrics.py │ └── rnc.py │ └── replacement.py ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── requirements.txt ├── test_many.py ├── test_multi.py ├── test_single.py └── test_spacing.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb linguist-detectable=false 2 | pymoode/backups/* linguist-detectable=false -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - deploy 6 | 7 | jobs: 8 | 9 | build: 10 | runs-on: "${{ matrix.os }}" 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: "Set up Python" 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: "${{ matrix.python-version }}" 17 | - name: "Install dependencies" 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install setuptools wheel numpy cython 21 | - uses: knicknic/os-specific-run@v1 22 | with: 23 | linux: "python setup.py sdist" 24 | macos: "python setup.py bdist_wheel" 25 | windows: "python setup.py bdist_wheel" 26 | - uses: actions/upload-artifact@v3 27 | with: 28 | name: dist 29 | path: dist 30 | strategy: 31 | matrix: 32 | os: 33 | - windows-latest 34 | - macos-latest 35 | python-version: 36 | - "3.7" 37 | - "3.8" 38 | - "3.9" 39 | - "3.10" 40 | - "3.11" 41 | - "3.12" 42 | include: 43 | - os: ubuntu-latest 44 | python-version: "3.12" 45 | 46 | build-manylinux: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v3 50 | - uses: RalfG/python-wheels-manylinux-build@v0.5.0 51 | with: 52 | build-requirements: "cython numpy" 53 | pip-wheel-args: "-w ./dist --no-deps" 54 | python-versions: "cp37-cp37m cp38-cp38 cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312" 55 | - name: "Remove non-compatible packages" 56 | run: "sudo rm dist/*linux_x86_64.whl\n" 57 | - uses: actions/upload-artifact@v3 58 | with: 59 | name: dist 60 | path: dist -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | - main 7 | 8 | jobs: 9 | 10 | testing: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install Python 3 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: '3.11' 18 | - name: Install Dependencies 19 | run: | 20 | pip install numpy Cython 21 | pip install -r tests/requirements.txt 22 | pip install pytest 23 | - name: Install pymoode (DEBUG) 24 | run: | 25 | python setup.py install 26 | - name: Install pymoode 27 | run: | 28 | pip install . 29 | rm -rf pymoode 30 | - name: Check Compilation 31 | run: | 32 | python -c "from pymoode.survival.rank_and_crowding.metrics import IS_COMPILED;print('Compiled Extensions: ', IS_COMPILED)" 33 | - name: Run Tests 34 | run: | 35 | pytest -v --maxfail 1 --no-header -m "not long" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | dist.* 4 | experimental 5 | *.egg-info 6 | /venv 7 | */venv 8 | /pymoode/cython/*.html 9 | /pymoode/cython/*.pyd 10 | __pycache__/ 11 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/source/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | formats: 27 | - pdf 28 | - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | python: 34 | install: 35 | - requirements: docs/requirements.txt 36 | - method: pip 37 | path: . -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include pymoode *.py *.pyx *.pxd 2 | recursive-exclude pymoode *.so *.cpp *.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | header 4 | 5 |

6 | 7 | # pymoode 8 | 9 | A Python framework for Differential Evolution using [pymoo](https://github.com/anyoptimization/pymoo) (Blank & Deb, 2020). 10 | 11 | [Read the Docs](https://pymoode.readthedocs.io/en/latest/) 12 | 13 | ## Contents 14 | 15 | [Install](#install) | [Algorithms](#algorithms) | [Survival Operators](#survival-operators) | [Crowding Metrics](#crowding-metrics) | [Usage](#usage) | [Structure](#structure) | [Citation](#citation) | [References](#references) | [Contact](#contact) | [Acknowledgements](#acknowledgements) 16 | 17 | ## Install 18 | 19 | First, make sure you have a Python 3 environment installed. 20 | 21 | From PyPi: 22 | ``` 23 | pip install pymoode 24 | ``` 25 | 26 | From the current version on github: 27 | ``` 28 | pip install git+https://github.com/mooscaliaproject/pymoode 29 | ``` 30 | 31 | ## Algorithms 32 | - **DE**: Differential Evolution for single-objective problems proposed by Storn & Price (1997). Other features later implemented are also present, such as dither, jitter, selection variants, and crossover strategies. For details see Price et al. (2005). 33 | - **NSDE**: Non-dominated Sorting Differential Evolution, a multi-objective algorithm that combines DE mutation and crossover operators to NSGA-II (Deb et al., 2002) survival. 34 | - **GDE3**: Generalized Differential Evolution 3, a multi-objective algorithm that combines DE mutation and crossover operators to NSGA-II survival with a hybrid type survival strategy. In this algorithm, individuals might be removed in a one-to-one comparison before truncating the population by the multi-objective survival operator. It was proposed by Kukkonen, S. & Lampinen, J. (2005). Variants with M-Nearest Neighbors and 2-Nearest Neighbors survival are also available. 35 | - **NSDE-R**: Non-dominated Sorting Differential Evolution based on Reference directions (Reddy & Dulikravich, 2019). It is an algorithm for many-objective problems that works as an extension of NSDE using NSGA-III (Deb & Jain, 2014) survival strategy. 36 | 37 | ## Survival Operators 38 | 39 | - **RandAndCrowding**: Flexible structure to implement NSGA-II rank and crowding survival with different options for crowding metric and elimination of individuals. 40 | - **ConstrRankAndCrowding**: A survival operator based on rank and crowding with a special constraint handling approach proposed by Kukkonen, S. & Lampinen, J. (2005). 41 | 42 | ## Crowding Metrics 43 | 44 | - **Crowding Distance** (*'cd'*): Proposed by Deb et al. (2002) in NSGA-II. Imported from *pymoo*. 45 | - **Pruning Crowding Distance** (*'pruning-cd'* or *'pcd'*): Proposed by Kukkonen & Deb (2006a), it recursively recalculates crowding distances as removes individuals from a population to improve diversity. 46 | - ***M*-Nearest Neighbors** (*'mnn'*): Proposed by Kukkonen & Deb (2006b) in an extension of GDE3 to many-objective problems. 47 | - **2-Nearest Neighbors** (*'2nn'*): Also proposed by Kukkonen & Deb (2006b), it is a variant of M-Nearest Neighbors in which the number of neighbors is two. 48 | - **Crowding Entropy** (*'ce'*): Proposed by Wang et al. (2010) in MOSADE. 49 | 50 | Metrics *'pcd'*, *'mnn'*, and *'2nn'* are recursively recalculated as individuals are removed, to improve the population diversity. Therefore, they are implemented using cython to reduce computational time. If compilation fails, .py files are used instead, which makes it slightly slower. 51 | 52 | ## Usage 53 | For more examples, [read the docs](https://pymoode.readthedocs.io/en/latest/Usage/index.html) 54 | 55 | ```python 56 | import matplotlib.pyplot as plt 57 | from pymoo.problems import get_problem 58 | from pymoo.optimize import minimize 59 | from pymoode.algorithms import GDE3 60 | from pymoode.survival import RankAndCrowding 61 | 62 | problem = get_problem("tnk") 63 | pf = problem.pareto_front() 64 | ``` 65 | 66 | ```python 67 | gde3 = GDE3( 68 | pop_size=50, variant="DE/rand/1/bin", CR=0.5, F=(0.0, 0.9), 69 | survival=RankAndCrowding(crowding_func="pcd") 70 | ) 71 | 72 | res = minimize(problem, gde3, ('n_gen', 200), seed=12) 73 | ``` 74 | 75 | ```python 76 | fig, ax = plt.subplots(figsize=[6, 5], dpi=100) 77 | ax.scatter(pf[:, 0], pf[:, 1], color="navy", label="True Front") 78 | ax.scatter(res.F[:, 0], res.F[:, 1], color="firebrick", label="GDE3") 79 | ax.set_ylabel("$f_2$") 80 | ax.set_xlabel("$f_1$") 81 | ax.legend() 82 | fig.tight_layout() 83 | plt.show() 84 | ``` 85 | 86 |

87 | tnk_gde3 91 |

92 | 93 | Alternatively, on the three-objective problem DTLZ2, it would produce amazing results. 94 | 95 | ```python 96 | problem = get_problem("dtlz2") 97 | ``` 98 | 99 | ```python 100 | gde3mnn = GDE3( 101 | pop_size=150, variant="DE/rand/1/bin", CR=0.5, F=(0.0, 0.9), 102 | survival=RankAndCrowding(crowding_func="mnn") 103 | ) 104 | 105 | res = minimize(problem, gde3mnn, ('n_gen', 250), seed=12) 106 | ``` 107 | 108 |

109 | gde3_dtlz2 113 |

114 | 115 | ## Structure 116 | 117 | ``` 118 | pymoode 119 | ├───algorithms 120 | │ ├───DE 121 | │ ├───GDE3 122 | │ ├───NSDE 123 | │ └───NSDER 124 | ├───survival 125 | │ ├───RankAndCrowding 126 | │ └───ConstrRankAndCrowding 127 | ├───performance 128 | │ └───SpacingIndicator 129 | └───operators 130 | ├───dem.py 131 | │ └───DEM 132 | ├───dex.py 133 | │ └───DEX 134 | └───des.py 135 | └───DES 136 | ``` 137 | 138 | 139 | ## Citation 140 | This package was developed as part of an academic optimization project. Please, if you use it for research purposes, cite it using the published article: 141 | 142 | [Leite, B., Costa, A. O. S., Costa, E. F., 2023. Multi-objective optimization of adiabatic styrene reactors using Generalized Differential Evolution 3 (GDE3). Chem. Eng. Sci., Volume 265, Article 118196. doi:10.1016/j.ces.2022.118196.](https://doi.org/10.1016/j.ces.2022.118196) 143 | 144 | ## References 145 | 146 | [Blank, J. & Deb, K., 2020. pymoo: Multi-Objective Optimization in Python. IEEE Access, Volume 8, pp. 89497-89509. doi:10.1109/ACCESS.2020.2990567.](https://doi.org/10.1109/ACCESS.2020.2990567) 147 | 148 | [Deb, K. & Jain, H., 2014. An evolutionary many-objective optimization algorithm using reference-point-based nondominated sorting approach, part I: solving problems with box constraints. IEEE Transactions on Evolutionary Computation, 18(4), pp. 577–601. doi:10.1109/TEVC.2013.2281535.](https://doi.org/10.1109/TEVC.2013.2281535) 149 | 150 | [Deb, K., Pratap, A., Agarwal, S. & Meyarivan, T. A. M. T., 2002. A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II. IEEE transactions on evolutionary computation, 6(2), pp. 182-197. doi:10.1109/4235.996017.](https://doi.org/10.1109/4235.996017) 151 | 152 | [Kukkonen, S. & Deb, K., 2006a. Improved Pruning of Non-Dominated Solutions Based on Crowding Distance for Bi-Objective Optimization Problems. Vancouver, s.n., pp. 1179-1186. doi:10.1109/CEC.2006.1688443.](https://doi.org/10.1109/CEC.2006.1688443) 153 | 154 | [Kukkonen, S. & Deb, K., 2006b. A fast and effective method for pruning of non-dominated solutions in many-objective problems. In: Parallel problem solving from nature-PPSN IX. Berlin: Springer, pp. 553-562. doi:10.1007/11844297_56.](https://doi.org/10.1007/11844297_56) 155 | 156 | [Kukkonen, S. & Lampinen, J., 2005. GDE3: The third evolution step of generalized differential evolution. 2005 IEEE congress on evolutionary computation, Volume 1, pp. 443-450. doi:10.1109/CEC.2005.1554717.](https://doi.org/10.1109/CEC.2005.1554717) 157 | 158 | [Reddy, S. R. & Dulikravich, G. S., 2019. Many-objective differential evolution optimization based on reference points: NSDE-R. Struct. Multidisc. Optim., Volume 60, pp. 1455-1473. doi:10.1007/s00158-019-02272-0.](https://doi.org/10.1007/s00158-019-02272-0) 159 | 160 | Price, K. V., Storn, R. M. & Lampinen, J. A., 2005. Differential Evolution: A Practical Approach to Global Optimization. 1st ed. Springer: Berlin. 161 | 162 | [Storn, R. & Price, K., 1997. Differential evolution–a simple and efficient heuristic for global optimization over continuous spaces. J. Glob. Optim., 11(4), pp. 341-359. doi:10.1023/A:1008202821328](https://doi.org/10.1023/A:1008202821328) 163 | 164 | [Wang, Y.-N., Wu, L.-H. & Yuan, X.-F., 2010. Multi-objective self-adaptive differential evolution with elitist archive and crowding entropy-based diversity measure. Soft Comput., 14(3), pp. 193-209. doi:10.1007/s00500-008-0394-9](https://doi.org/10.1007/s00500-008-0394-9) 165 | 166 | ## Contact 167 | e-mail: bruscalia12@gmail.com 168 | 169 | ## Acknowledgements 170 | To Julian Blank, who created the amazing structure of pymoo, making such a project possible. 171 | 172 | To Esly F. da Costa Junior, for the unconditional support all along. 173 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | ipykernel 2 | pymoode 3 | sphinx 4 | sphinx_rtd_theme 5 | sphinxcontrib-bibtex 6 | pandoc 7 | numpydoc 8 | nbsphinx 9 | matplotlib 10 | -------------------------------------------------------------------------------- /docs/source/Theory/Survival.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "# Survival\n", 9 | "\n", 10 | "The general theory of pymoode's survival operators is presented throughout this section, although code details should be better addressed in their [corresponding section](../pymoode.survival.rst)." 11 | ] 12 | }, 13 | { 14 | "attachments": {}, 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "## Single-objective\n", 19 | "\n", 20 | "In single-objective DE, to decide if a trial vector $u_i$ should become a member of the next generation, it is compared to the corresponding target vector $x_i$ using a greedy criterion . If $u_i$ outperforms $x_i$, it proceeds to the next generation and $x_i$ is removed.\n", 21 | "\n", 22 | "Until version 0.2.4, the one-to-one comparison in DE was performed using pymoo's ``ImprovementReplacement`` class. Now the user can parse its own survival operator to single-objective DE via the ``survival`` argument. It is recommended that the new operator is inherited from ``pymoode.survival.replacement.BaseReplacement``.\n", 23 | "\n", 24 | "

\n", 25 | " \"rastrigin\"\n", 27 | "

" 28 | ] 29 | }, 30 | { 31 | "attachments": {}, 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "## Multi-objective algorithms\n", 36 | "\n", 37 | "The one-to-one comparison might be inadequate in multi-objective problems and is usually replaced by other survival strategies.\n", 38 | "\n", 39 | "GDE3 and NSDE are two multi-objective variants of DE using Nondominated Sorting and crowding metrics in the survival stage. They share the same reproduction operators, although different in their survival strategies.\n", 40 | "\n", 41 | "Whereas NSDE uses a full (µ+λ) survival strategy, which combines the parent population with offspring, GDE3 has a one-to-one comparison between each member in the parent population and its corresponding trial vector before truncating the population into the original size. In this one-to-one comparison, one of the individuals of each pair might be removed in advance from the combined population before (µ+λ) survival if dominated by the other.\n", 42 | "\n", 43 | "

\n", 44 | " \"tnk_gde3\"\n", 46 | "

" 47 | ] 48 | }, 49 | { 50 | "attachments": {}, 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "## Many-objective algorithms\n", 55 | "\n", 56 | "As GDE3 and NSDE have been originally designed using NSGA-II Rank and Crowding survival, they perform poorly in problems with more than two objectives. However, simple modifications from a user perspective can solve this issue. Let us in pymoode denote problems with more than two objectives as many-objective problems.\n", 57 | "\n", 58 | "NSDE-R combines the survival operator of NSGA-III with the reproduction operators of DE , which leads to great performance in many-objective problems. GDE3-MNN is a variant of GDE3 proposed by Kukkonen and Deb that replaces original crowding distances of Rank and Crowding survival with an M-Nearest Neighbors based crowding metric with recursive elimination and re-calculation. It has improved a lot the performance of GDE3 in many-objective problems.\n", 59 | "\n", 60 | "

\n", 61 | " \"tnk_gde3\"\n", 63 | "

" 64 | ] 65 | } 66 | ], 67 | "metadata": { 68 | "kernelspec": { 69 | "display_name": "venv", 70 | "language": "python", 71 | "name": "python3" 72 | }, 73 | "language_info": { 74 | "codemirror_mode": { 75 | "name": "ipython", 76 | "version": 3 77 | }, 78 | "file_extension": ".py", 79 | "mimetype": "text/x-python", 80 | "name": "python", 81 | "nbconvert_exporter": "python", 82 | "pygments_lexer": "ipython3", 83 | "version": "3.9.7 (tags/v3.9.7:1016ef3, Aug 30 2021, 20:19:38) [MSC v.1929 64 bit (AMD64)]" 84 | }, 85 | "orig_nbformat": 4, 86 | "vscode": { 87 | "interpreter": { 88 | "hash": "b918b2b0974b3df50d2cb6c751b8eaae1bb466aaf5007e6a0fab3d6d8f3e624b" 89 | } 90 | } 91 | }, 92 | "nbformat": 4, 93 | "nbformat_minor": 2 94 | } 95 | -------------------------------------------------------------------------------- /docs/source/Theory/plots.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | 3 | 4 | def plot_pairs_3d(first, second, colors=("indigo", "firebrick"), **kwargs): 5 | 6 | fig, ax = plt.subplots(1, 2, subplot_kw={'projection':'3d'}, **kwargs) 7 | 8 | ax[0].scatter( 9 | *first[1].T, 10 | color=colors[0], label=first[0], marker="o", 11 | ) 12 | ax[0].set_ylabel("$f_2$") 13 | ax[0].set_xlabel("$f_1$") 14 | ax[0].set_zlabel("$f_3$") 15 | ax[0].legend() 16 | 17 | ax[1].scatter( 18 | *second[1].T, 19 | color=colors[1], label=second[0], marker="o", 20 | ) 21 | ax[1].set_ylabel("$f_2$") 22 | ax[1].set_xlabel("$f_1$") 23 | ax[1].set_zlabel("$f_3$") 24 | ax[1].legend() 25 | 26 | ax[0].view_init(elev=30, azim=30) 27 | ax[1].view_init(elev=30, azim=30) 28 | 29 | fig.tight_layout() 30 | plt.show() 31 | 32 | 33 | def plot_pairs_2d(first, second, colors=("indigo", "firebrick"), **kwargs): 34 | 35 | fig, ax = plt.subplots(1, 2, **kwargs) 36 | 37 | ax[0].scatter( 38 | *first[1].T, 39 | color=colors[0], label=first[0], marker="o", 40 | ) 41 | ax[0].set_ylabel("$f_2$") 42 | ax[0].set_xlabel("$f_1$") 43 | ax[0].legend() 44 | 45 | ax[1].scatter( 46 | *second[1].T, 47 | color=colors[1], label=second[0], marker="o", 48 | ) 49 | ax[1].set_ylabel("$f_2$") 50 | ax[1].set_xlabel("$f_1$") 51 | ax[1].legend() 52 | 53 | fig.tight_layout() 54 | plt.show() -------------------------------------------------------------------------------- /docs/source/Usage/Single-objective.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "# Single-objective" 9 | ] 10 | }, 11 | { 12 | "attachments": {}, 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "Throughout this section, we will solve the classic Rastrigin problem imported from [pymoo](https://github.com/anyoptimization/pymoo/blob/main/pymoo/problems/single/rastrigin.py). Three strategies of how to instantiate it from scratch are also presented in the [complete tutorial](./Complete-tutorial.ipynb).\n", 17 | "\n", 18 | "Besides the original DE , in this example the PSO implementation from pymoo will also be used." 19 | ] 20 | }, 21 | { 22 | "attachments": {}, 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "**Rastrigin**\n", 27 | "\n", 28 | "$$\n", 29 | "\\begin{align}\n", 30 | " \\text{min} \\; \\; & f(\\boldsymbol{x}) = An + \\sum_{i=1}^{n}[x_i^2 - A \\, \\mathrm{cos}(2 \\pi x_i)]\\\\\n", 31 | " \\text{s.t.} \\; \\; & -5.12 \\leq x_i \\leq 5.12 & \\forall i \\in \\{ 1, 2 \\}\n", 32 | "\\end{align}\n", 33 | "$$" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 1, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "from pymoo.algorithms.soo.nonconvex.pso import PSO\n", 43 | "from pymoo.optimize import minimize\n", 44 | "from pymoo.problems import get_problem\n", 45 | "from pymoode.algorithms import GDE3\n", 46 | "from pymoode.algorithms import DE" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 2, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "problem = get_problem(\"rastrigin\")" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 3, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "NGEN = 100\n", 65 | "POPSIZE = 20\n", 66 | "SEED = 3" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 4, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "# DE Parameters\n", 76 | "CR = 0.5\n", 77 | "F = (0.3, 1.0)" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 5, 83 | "metadata": {}, 84 | "outputs": [ 85 | { 86 | "name": "stdout", 87 | "output_type": "stream", 88 | "text": [ 89 | "[1.42108547e-14]\n", 90 | "[-9.85467210e-11 8.38205833e-09]\n" 91 | ] 92 | } 93 | ], 94 | "source": [ 95 | "de = DE(pop_size=POPSIZE, variant=\"DE/rand/1/bin\", CR=CR, F=F)\n", 96 | "\n", 97 | "res_de = minimize(\n", 98 | " problem,\n", 99 | " de,\n", 100 | " ('n_gen', NGEN),\n", 101 | " seed=SEED,\n", 102 | " save_history=False,\n", 103 | " verbose=False,\n", 104 | ")\n", 105 | "\n", 106 | "print(res_de.F)\n", 107 | "print(res_de.X)" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 6, 113 | "metadata": {}, 114 | "outputs": [ 115 | { 116 | "name": "stdout", 117 | "output_type": "stream", 118 | "text": [ 119 | "[2.00159889e-11]\n", 120 | "[-1.28686492e-07 2.90416106e-07]\n" 121 | ] 122 | } 123 | ], 124 | "source": [ 125 | "pso = PSO(pop_size=POPSIZE)\n", 126 | "\n", 127 | "res_pso = minimize(\n", 128 | " problem,\n", 129 | " pso,\n", 130 | " ('n_gen', NGEN),\n", 131 | " seed=SEED,\n", 132 | " save_history=False,\n", 133 | " verbose=False,\n", 134 | ")\n", 135 | "\n", 136 | "print(res_pso.F)\n", 137 | "print(res_pso.X)" 138 | ] 139 | } 140 | ], 141 | "metadata": { 142 | "kernelspec": { 143 | "display_name": "venv", 144 | "language": "python", 145 | "name": "python3" 146 | }, 147 | "language_info": { 148 | "codemirror_mode": { 149 | "name": "ipython", 150 | "version": 3 151 | }, 152 | "file_extension": ".py", 153 | "mimetype": "text/x-python", 154 | "name": "python", 155 | "nbconvert_exporter": "python", 156 | "pygments_lexer": "ipython3", 157 | "version": "3.9.7 (tags/v3.9.7:1016ef3, Aug 30 2021, 20:19:38) [MSC v.1929 64 bit (AMD64)]" 158 | }, 159 | "orig_nbformat": 4, 160 | "vscode": { 161 | "interpreter": { 162 | "hash": "6f648821e8b7b21234539644f05b8ed9c4fec624e513ce0931c304e76320dc94" 163 | } 164 | } 165 | }, 166 | "nbformat": 4, 167 | "nbformat_minor": 2 168 | } 169 | -------------------------------------------------------------------------------- /docs/source/Usage/index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "# Usage\n", 9 | "\n", 10 | "Here are linked examples for:\n", 11 | "\n", 12 | "- [Single-objective problems](./Single-objective.ipynb)\n", 13 | "- [Multi-objective problems](./Multi-objective.ipynb)\n", 14 | "- [Many-objective problems](./Many-objective.ipynb)\n", 15 | "- [Complete tutorial](./Complete-tutorial.ipynb)\n", 16 | "- [Portfolio selection](./Portfolio.ipynb)\n", 17 | "\n", 18 | "Enjoy!" 19 | ] 20 | } 21 | ], 22 | "metadata": { 23 | "kernelspec": { 24 | "display_name": "venv", 25 | "language": "python", 26 | "name": "python3" 27 | }, 28 | "language_info": { 29 | "name": "python", 30 | "version": "3.9.7 (tags/v3.9.7:1016ef3, Aug 30 2021, 20:19:38) [MSC v.1929 64 bit (AMD64)]" 31 | }, 32 | "orig_nbformat": 4, 33 | "vscode": { 34 | "interpreter": { 35 | "hash": "b918b2b0974b3df50d2cb6c751b8eaae1bb466aaf5007e6a0fab3d6d8f3e624b" 36 | } 37 | } 38 | }, 39 | "nbformat": 4, 40 | "nbformat_minor": 2 41 | } 42 | -------------------------------------------------------------------------------- /docs/source/algorithms.rst: -------------------------------------------------------------------------------- 1 | Algorithms 2 | ========== 3 | 4 | The general overview of algorithms is presented in this section, although for more usage details we 5 | suggest referring to `usage page `_. 6 | 7 | 8 | DE 9 | -- 10 | 11 | Differential Evolution for single-objective problems proposed by 12 | Storn & Price [:cite:label:`de_article`]. 13 | Other features later implemented are also present, such as dither, jitter, selection variants, 14 | and crossover strategies. For details see Price et al. [:cite:label:`de_book`]. 15 | 16 | 17 | GDE3 18 | ---- 19 | 20 | Generalized Differential Evolution 3, a multi-objective algorithm that combines 21 | DE mutation and crossover operators to NSGA-II [:cite:label:`nsga2`] survival with a hybrid type survival strategy. 22 | In this algorithm, individuals might be removed in a one-to-one comparison before truncating 23 | the population by the multi-objective survival operator. It was proposed by 24 | Kukkonen, S. & Lampinen, J. [:cite:label:`gde3`]. 25 | Variants with M-Nearest Neighbors and 2-Nearest Neighbors survival [:cite:label:`gde3many`] 26 | are also available. 27 | 28 | 29 | NSDE 30 | ---- 31 | 32 | Non-dominated Sorting Differential Evolution, a multi-objective algorithm that combines 33 | DE mutation and crossover operators to NSGA-II [:cite:label:`nsga2`] survival. 34 | 35 | 36 | NSDE-R 37 | ------ 38 | 39 | Non-dominated Sorting Differential Evolution based on Reference directions [:cite:label:`nsder`]. 40 | It is an algorithm for many-objective problems that works as an extension of NSDE 41 | using NSGA-III [:cite:label:`nsga3-part1`] [:cite:label:`nsga3-part2`] survival strategy. -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'pymoode' 10 | copyright = '2023, Bruno Scalia C. F. Leite' 11 | author = 'Bruno Scalia C. F. Leite' 12 | release = '0.2.4.1' 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = [ 18 | 'sphinx.ext.mathjax', 19 | 'sphinx.ext.autodoc', 20 | 'sphinx.ext.autosummary', 21 | 'numpydoc', 22 | 'nbsphinx', 23 | 'sphinx.ext.intersphinx', 24 | 'sphinx.ext.coverage', 25 | 'matplotlib.sphinxext.plot_directive', 26 | 'IPython.sphinxext.ipython_console_highlighting', 27 | 'sphinxcontrib.bibtex', 28 | ] 29 | 30 | bibtex_bibfiles = ['references.bib'] 31 | bibtex_reference_style = 'label' 32 | 33 | templates_path = ['_templates'] 34 | exclude_patterns = [] 35 | autoclass_content = 'both' 36 | 37 | intersphinx_mapping = { 38 | 'python': ('http://docs.python.org/2', None), 39 | 'numpy': ('http://docs.scipy.org/doc/numpy', "http://docs.scipy.org/doc/numpy/objects.inv"), 40 | 'scipy': ('http://docs.scipy.org/doc/scipy/reference', None), 41 | 'matplotlib': ('http://matplotlib.sourceforge.net', None), 42 | 'pymoo': ('http://pymoo.org', None) 43 | } 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 48 | 49 | html_theme = 'sphinx_rtd_theme' 50 | html_static_path = ['_static'] 51 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pymoode documentation master file, created by 2 | sphinx-quickstart on Tue Jan 3 02:07:17 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. image:: https://github.com/mooscaliaproject/pymoode/raw/main/images/logo_header_pymoode.png 7 | :alt: header 8 | :align: center 9 | :target: https://github.com/mooscaliaproject/pymoode 10 | 11 | 12 | pymoode: Differential Evolution in Python 13 | ========================================= 14 | 15 | A Python framework for Differential Evolution using `pymoo `_ [:cite:label:`pymoo`]. 16 | 17 | 18 | Install 19 | ------- 20 | 21 | First, make sure you have a Python 3 environment installed. 22 | 23 | From PyPi: 24 | 25 | .. code:: bash 26 | 27 | pip install pymoode 28 | 29 | 30 | From the current version on github: 31 | 32 | .. code:: bash 33 | 34 | pip install -e git+https://github.com/mooscalia/pymoode#egg=pymoode 35 | 36 | 37 | New features 38 | ------------ 39 | 40 | This package was written as an extension of pymoo, providing some additional 41 | features for DE algorithms and survival operators. One might refer to the sections 42 | :doc:`Algorithms `, `Survival `_ and 43 | `Rank and Crowding `_ for more details. 44 | 45 | For instance, these solutions for the DTLZ2 problem were obtained using GDE3 with the M-Nearest Neighbors crowding metric. 46 | 47 | .. image:: https://github.com/mooscaliaproject/pymoode/raw/main/images/gde3mnn_example.gif 48 | :alt: header 49 | :align: center 50 | :width: 500 51 | :target: https://github.com/mooscaliaproject/pymoode 52 | 53 | 54 | .. toctree:: 55 | :maxdepth: 2 56 | :caption: Algorithms 57 | 58 | algorithms 59 | 60 | 61 | .. toctree:: 62 | :maxdepth: 1 63 | :caption: Usage 64 | 65 | Usage/Complete-tutorial.ipynb 66 | Usage/Single-objective.ipynb 67 | Usage/Multi-objective.ipynb 68 | Usage/Many-objective.ipynb 69 | Usage/Portfolio.ipynb 70 | 71 | 72 | .. toctree:: 73 | :maxdepth: 1 74 | :caption: Theory 75 | 76 | Theory/DE.ipynb 77 | Theory/Survival.ipynb 78 | Theory/Rank-and-Crowding.ipynb 79 | 80 | 81 | .. toctree:: 82 | :maxdepth: 3 83 | :caption: Package 84 | 85 | modules 86 | 87 | 88 | Citation 89 | ======== 90 | 91 | This package was developed as part of an academic optimization project [:cite:label:`pymoodestyrene`], as well as pymoo [:cite:label:`pymoo`]. 92 | Please, if you use it for research purposes, cite it accordingly: 93 | 94 | `Blank, J. & Deb, K., 2020. pymoo: Multi-Objective Optimization in Python. IEEE Access, Volume 8, pp. 89497-89509. doi:10.1109/ACCESS.2020.2990567. `_ 95 | 96 | `Leite, B., Costa, A. O. S., Costa, E. F., 2023. Multi-objective optimization of adiabatic styrene reactors using Generalized Differential Evolution 3 (GDE3). Chem. Eng. Sci., Volume 265, Article 118196. doi:10.1016/j.ces.2022.118196. `_ 97 | 98 | 99 | References 100 | ========== 101 | 102 | To the complete reference list, please refer to :doc:`this page `. 103 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | pymoode 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | pymoode.algorithms 8 | pymoode.survival 9 | pymoode.performance 10 | pymoode.operators 11 | -------------------------------------------------------------------------------- /docs/source/pymoode.algorithms.rst: -------------------------------------------------------------------------------- 1 | Algorithms 2 | ========== 3 | 4 | pymoode.algorithms 5 | ------------------ 6 | 7 | .. automodule:: pymoode.algorithms 8 | :members: DE, GDE3, NSDE, NSDER 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | 13 | pymoode.algorithms.base 14 | ----------------------- 15 | 16 | .. autoclass:: pymoode.algorithms.base.evolutionary.EvolutionaryAlgorithm 17 | .. autoclass:: pymoode.algorithms.base.genetic.GeneticAlgorithm 18 | 19 | .. automodule:: pymoode.algorithms.base.genetic 20 | :members: 21 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/pymoode.cython.rst: -------------------------------------------------------------------------------- 1 | pymoode.cython package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pymoode.cython.info module 8 | -------------------------- 9 | 10 | .. automodule:: pymoode.cython.info 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pymoode.cython.mnn module 16 | ------------------------- 17 | 18 | .. automodule:: pymoode.cython.mnn 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | pymoode.cython.pruning\_cd module 24 | --------------------------------- 25 | 26 | .. automodule:: pymoode.cython.pruning_cd 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | pymoode.cython.spacing\_neighbors module 32 | ---------------------------------------- 33 | 34 | .. automodule:: pymoode.cython.spacing_neighbors 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | Module contents 40 | --------------- 41 | 42 | .. automodule:: pymoode.cython 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | -------------------------------------------------------------------------------- /docs/source/pymoode.misc.rst: -------------------------------------------------------------------------------- 1 | pymoode.misc package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pymoode.misc.mnn module 8 | ----------------------- 9 | 10 | .. automodule:: pymoode.misc.mnn 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pymoode.misc.pruning\_cd module 16 | ------------------------------- 17 | 18 | .. automodule:: pymoode.misc.pruning_cd 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: pymoode.misc 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docs/source/pymoode.operators.rst: -------------------------------------------------------------------------------- 1 | Operators 2 | ========= 3 | 4 | pymoode.operators.des 5 | --------------------- 6 | 7 | .. autoclass:: pymoode.operators.des.DES 8 | 9 | 10 | pymoode.operators.dex 11 | --------------------- 12 | 13 | .. autoclass:: pymoode.operators.dex.DEX 14 | .. autoclass:: pymoode.operators.dex.DEM 15 | 16 | -------------------------------------------------------------------------------- /docs/source/pymoode.performance.rst: -------------------------------------------------------------------------------- 1 | Indicators 2 | ========== 3 | 4 | pymoode.performance 5 | ------------------- 6 | 7 | .. autoclass:: pymoode.performance.SpacingIndicator 8 | -------------------------------------------------------------------------------- /docs/source/pymoode.rst: -------------------------------------------------------------------------------- 1 | pymoode 2 | ======= 3 | 4 | Modules 5 | ------- 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | pymoode.algorithms 11 | pymoode.operators 12 | pymoode.performance 13 | pymoode.survival 14 | 15 | -------------------------------------------------------------------------------- /docs/source/pymoode.survival.rst: -------------------------------------------------------------------------------- 1 | Survival 2 | ======== 3 | 4 | pymoode.survival 5 | ---------------- 6 | 7 | Rank and Crowding 8 | ~~~~~~~~~~~~~~~~~ 9 | 10 | .. autoclass:: pymoode.survival.RankAndCrowding 11 | .. autoclass:: pymoode.survival.ConstrRankAndCrowding 12 | 13 | 14 | Replacement 15 | ~~~~~~~~~~~ 16 | 17 | .. autoclass:: pymoode.survival.replacement.BaseReplacement 18 | .. autoclass:: pymoode.survival.replacement.ImproveReplacement -------------------------------------------------------------------------------- /docs/source/references.bib: -------------------------------------------------------------------------------- 1 | @article{pymoo, 2 | author={J. {Blank} and K. {Deb}}, 3 | journal={IEEE Access}, 4 | title={{p}ymoo: Multi-Objective Optimization in Python}, 5 | year={2020}, 6 | volume={8}, 7 | pages={89497-89509}, 8 | doi={10.1109/ACCESS.2020.2990567} 9 | } 10 | 11 | 12 | @article{pymoodestyrene, 13 | title={Multi-objective optimization of adiabatic styrene reactors using Generalized Differential Evolution 3 ({GDE3})}, 14 | author={Leite, Bruno and da Costa, Andr{\'e}a Oliveira Souza and da Costa Junior, Esly Ferreira}, 15 | journal={Chemical Engineering Science}, 16 | volume={265}, 17 | pages={118196}, 18 | year={2023}, 19 | publisher={Elsevier}, 20 | doi={10.1016/j.ces.2022.118196} 21 | } 22 | 23 | 24 | @article{de_article, 25 | title={Differential evolution--a simple and efficient heuristic for global optimization over continuous spaces}, 26 | author={Storn, Rainer and Price, Kenneth}, 27 | journal={Journal of global optimization}, 28 | volume={11}, 29 | number={4}, 30 | pages={341--359}, 31 | year={1997}, 32 | publisher={Springer} 33 | } 34 | 35 | 36 | @book{de_book, 37 | title={Differential evolution: a practical approach to global optimization}, 38 | author={Price, Kenneth and Storn, Rainer M and Lampinen, Jouni A}, 39 | year={2005}, 40 | publisher={Springer Science \& Business Media} 41 | } 42 | 43 | 44 | @book{multi_objective_book, 45 | author={Deb, Kalyanmoy and Kalyanmoy, Deb}, 46 | title={Multi-Objective Optimization Using Evolutionary Algorithms}, 47 | year={2001}, 48 | isbn={047187339X}, 49 | publisher={John Wiley \& Sons, Inc.}, 50 | address={New York, NY, USA}, 51 | } 52 | 53 | 54 | @article{nsga2, 55 | author={Deb, K. and Pratap, A. and Agarwal, S. and Meyarivan, T.}, 56 | title={A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-{II}}, 57 | journal={Trans. Evol. Comp}, 58 | issue_date={April 2002}, 59 | volume={6}, 60 | number={2}, 61 | year={2002}, 62 | issn={1089-778X}, 63 | pages={182--197}, 64 | numpages={16}, 65 | url={http://dx.doi.org/10.1109/4235.996017}, 66 | doi={10.1109/4235.996017}, 67 | acmid={2221582}, 68 | publisher={IEEE Press}, 69 | address={Piscataway, NJ, USA}, 70 | } 71 | 72 | 73 | @article{ref_dirs_blank, 74 | author={J. {Blank} and K. {Deb} and Y. {Dhebar} and S. {Bandaru} and H. {Seada}}, 75 | journal={IEEE Transactions on Evolutionary Computation}, 76 | title={Generating Well-Spaced Points on a Unit Simplex for Evolutionary Many-Objective Optimization}, 77 | year={2021}, 78 | volume={25}, 79 | number={1}, 80 | pages={48-60}, 81 | doi={10.1109/TEVC.2020.2992387} 82 | } 83 | 84 | 85 | @inproceedings{gde3, 86 | title={GDE3: The third evolution step of generalized differential evolution}, 87 | author={Kukkonen, Saku and Lampinen, Jouni}, 88 | booktitle={2005 IEEE congress on evolutionary computation}, 89 | volume={1}, 90 | pages={443--450}, 91 | year={2005}, 92 | organization={IEEE} 93 | } 94 | 95 | 96 | @inproceedings{gde3pruning, 97 | title={Improved pruning of non-dominated solutions based on crowding distance for bi-objective optimization problems}, 98 | author={Kukkonen, Saku and Deb, Kalyanmoy}, 99 | booktitle={2006 IEEE International Conference on Evolutionary Computation}, 100 | pages={1179--1186}, 101 | year={2006}, 102 | organization={IEEE} 103 | } 104 | 105 | 106 | @incollection{gde3many, 107 | title={A fast and effective method for pruning of non-dominated solutions in many-objective problems}, 108 | author={Kukkonen, Saku and Deb, Kalyanmoy}, 109 | booktitle={Parallel problem solving from nature-PPSN IX}, 110 | pages={553--562}, 111 | year={2006}, 112 | publisher={Springer} 113 | } 114 | 115 | 116 | @article{mosade, 117 | title={Multi-objective self-adaptive differential evolution with elitist archive and crowding entropy-based diversity measure}, 118 | author={Wang, Yao-Nan and Wu, Liang-Hong and Yuan, Xiao-Fang}, 119 | journal={Soft Computing}, 120 | volume={14}, 121 | number={3}, 122 | pages={193--209}, 123 | year={2010}, 124 | publisher={Springer} 125 | } 126 | 127 | 128 | @article{nsder, 129 | title={Many-objective differential evolution optimization based on reference points: NSDE-R}, 130 | author={Reddy, Sohail R and Dulikravich, George S}, 131 | journal={Structural and Multidisciplinary Optimization}, 132 | volume={60}, 133 | number={4}, 134 | pages={1455--1473}, 135 | year={2019}, 136 | publisher={Springer} 137 | } 138 | 139 | 140 | @article{nsga3-part1, 141 | title={An evolutionary many-objective optimization algorithm using reference-point-based nondominated sorting approach, Part {I}: Solving problems with box constraints}, 142 | author={Kalyanmoy Deb and Himanshu Jain}, 143 | year={2014}, 144 | doi={10.1109/TEVC.2013.2281535}, 145 | volume={18}, 146 | pages={577-601}, 147 | journal={IEEE Transactions on Evolutionary Computation}, 148 | issn={1089-778X}, 149 | publisher={Institute of Electrical and Electronics Engineers Inc.}, 150 | number={4}, 151 | } 152 | 153 | 154 | @article{nsga3-part2, 155 | author={H. Jain and K. Deb}, 156 | journal={IEEE Transactions on Evolutionary Computation}, 157 | title={An Evolutionary Many-Objective Optimization Algorithm Using Reference-Point Based Nondominated Sorting Approach, Part {II}: Handling Constraints and Extending to an Adaptive Approach}, 158 | year={2014}, 159 | volume={18}, 160 | number={4}, 161 | pages={602-622}, 162 | ISSN={1089-778X}, 163 | } 164 | 165 | 166 | @article{de_crossover, 167 | title={Influence of crossover on the behavior of Differential Evolution Algorithms}, 168 | journal={Applied Soft Computing}, 169 | volume={9}, 170 | number={3}, 171 | pages={1126-1138}, 172 | year={2009}, 173 | issn={1568-4946}, 174 | doi={https://doi.org/10.1016/j.asoc.2009.02.012}, 175 | url={https://www.sciencedirect.com/science/article/pii/S1568494609000325}, 176 | author={Daniela Zaharie}, 177 | keywords={Differential Evolution, Binomial crossover, Exponential crossover, Parameter control, Self-adaptation}, 178 | } 179 | 180 | @article{pso, 181 | title={Particle swarm optimization}, 182 | author={Poli, Riccardo and Kennedy, James and Blackwell, Tim}, 183 | journal={Swarm intelligence}, 184 | volume={1}, 185 | number={1}, 186 | pages={33-57}, 187 | year={2007}, 188 | publisher={Springer} 189 | } 190 | 191 | 192 | @article{portfolio, 193 | ISSN={00221082, 15406261}, 194 | URL={http://www.jstor.org/stable/2975974}, 195 | author={Harry Markowitz}, 196 | journal={The Journal of Finance}, 197 | number={1}, 198 | pages={77-91}, 199 | publisher={[American Finance Association, Wiley]}, 200 | title={Portfolio Selection}, 201 | volume={7}, 202 | year={1952}, 203 | doi={10.2307/2975974}, 204 | } -------------------------------------------------------------------------------- /docs/source/references.rst: -------------------------------------------------------------------------------- 1 | References 2 | ========== 3 | 4 | 5 | .. only:: html 6 | 7 | .. bibliography:: 8 | :style: unsrt 9 | :all: 10 | -------------------------------------------------------------------------------- /images/gde3mnn_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/images/gde3mnn_example.gif -------------------------------------------------------------------------------- /images/gde3mnn_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/images/gde3mnn_example.png -------------------------------------------------------------------------------- /images/general_de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/images/general_de.png -------------------------------------------------------------------------------- /images/logo_header_pymoode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/images/logo_header_pymoode.png -------------------------------------------------------------------------------- /images/logo_pymoode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/images/logo_pymoode.png -------------------------------------------------------------------------------- /images/moo_gde3_f_video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/images/moo_gde3_f_video.gif -------------------------------------------------------------------------------- /images/rastrigin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/images/rastrigin.gif -------------------------------------------------------------------------------- /images/soo_video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/images/soo_video.gif -------------------------------------------------------------------------------- /images/tnk_gde3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/images/tnk_gde3.png -------------------------------------------------------------------------------- /notebooks/Single-objective.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "# Single-objective" 9 | ] 10 | }, 11 | { 12 | "attachments": {}, 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "Throughout this section, we will solve the classic Rastrigin problem imported from [pymoo](https://github.com/anyoptimization/pymoo/blob/main/pymoo/problems/single/rastrigin.py). Three strategies of how to instantiate it from scratch are also presented in the [complete tutorial](./Complete-tutorial.ipynb).\n", 17 | "\n", 18 | "Besides the original DE , in this example the PSO implementation from pymoo will also be used." 19 | ] 20 | }, 21 | { 22 | "attachments": {}, 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "**Rastrigin**\n", 27 | "\n", 28 | "$$\n", 29 | "\\begin{align}\n", 30 | " \\text{min} \\; \\; & f(\\boldsymbol{x}) = An + \\sum_{i=1}^{n}[x_i^2 - A \\, \\mathrm{cos}(2 \\pi x_i)]\\\\\n", 31 | " \\text{s.t.} \\; \\; & -5.12 \\leq x_i \\leq 5.12 & \\forall i \\in \\{ 1, 2 \\}\n", 32 | "\\end{align}\n", 33 | "$$" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 1, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "from pymoo.algorithms.soo.nonconvex.pso import PSO\n", 43 | "from pymoo.optimize import minimize\n", 44 | "from pymoo.problems import get_problem\n", 45 | "from pymoode.algorithms import DE" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 2, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "problem = get_problem(\"rastrigin\")" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 3, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "NGEN = 100\n", 64 | "POPSIZE = 20\n", 65 | "SEED = 3" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 4, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "# DE Parameters\n", 75 | "CR = 0.5\n", 76 | "F = (0.3, 1.0)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 5, 82 | "metadata": {}, 83 | "outputs": [ 84 | { 85 | "name": "stdout", 86 | "output_type": "stream", 87 | "text": [ 88 | "[1.42108547e-14]\n", 89 | "[-9.85467210e-11 8.38205833e-09]\n" 90 | ] 91 | } 92 | ], 93 | "source": [ 94 | "de = DE(pop_size=POPSIZE, variant=\"DE/rand/1/bin\", CR=CR, F=F)\n", 95 | "\n", 96 | "res_de = minimize(\n", 97 | " problem,\n", 98 | " de,\n", 99 | " ('n_gen', NGEN),\n", 100 | " seed=SEED,\n", 101 | " save_history=False,\n", 102 | " verbose=False,\n", 103 | ")\n", 104 | "\n", 105 | "print(res_de.F)\n", 106 | "print(res_de.X)" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 6, 112 | "metadata": {}, 113 | "outputs": [ 114 | { 115 | "name": "stdout", 116 | "output_type": "stream", 117 | "text": [ 118 | "[2.00159889e-11]\n", 119 | "[-1.28686492e-07 2.90416106e-07]\n" 120 | ] 121 | } 122 | ], 123 | "source": [ 124 | "pso = PSO(pop_size=POPSIZE)\n", 125 | "\n", 126 | "res_pso = minimize(\n", 127 | " problem,\n", 128 | " pso,\n", 129 | " ('n_gen', NGEN),\n", 130 | " seed=SEED,\n", 131 | " save_history=False,\n", 132 | " verbose=False,\n", 133 | ")\n", 134 | "\n", 135 | "print(res_pso.F)\n", 136 | "print(res_pso.X)" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [] 145 | } 146 | ], 147 | "metadata": { 148 | "kernelspec": { 149 | "display_name": "venv", 150 | "language": "python", 151 | "name": "python3" 152 | }, 153 | "language_info": { 154 | "codemirror_mode": { 155 | "name": "ipython", 156 | "version": 3 157 | }, 158 | "file_extension": ".py", 159 | "mimetype": "text/x-python", 160 | "name": "python", 161 | "nbconvert_exporter": "python", 162 | "pygments_lexer": "ipython3", 163 | "version": "3.9.7" 164 | }, 165 | "orig_nbformat": 4, 166 | "vscode": { 167 | "interpreter": { 168 | "hash": "6f648821e8b7b21234539644f05b8ed9c4fec624e513ce0931c304e76320dc94" 169 | } 170 | } 171 | }, 172 | "nbformat": 4, 173 | "nbformat_minor": 2 174 | } 175 | -------------------------------------------------------------------------------- /pymoode/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/pymoode/__init__.py -------------------------------------------------------------------------------- /pymoode/algorithms/__init__.py: -------------------------------------------------------------------------------- 1 | from pymoode.algorithms.de import DifferentialEvolution 2 | from pymoode.algorithms.de import DE 3 | from pymoode.algorithms.gde3 import GDE3, GDE3MNN, GDE32NN, GDE3P 4 | from pymoode.algorithms.nsde import NSDE 5 | from pymoode.algorithms.nsder import NSDER 6 | -------------------------------------------------------------------------------- /pymoode/algorithms/base/__init__.py: -------------------------------------------------------------------------------- 1 | from pymoode.algorithms.base.evolutionary import EvolutionaryAlgorithm 2 | from pymoode.algorithms.base.differential import DifferentialEvolution 3 | from pymoode.algorithms.base.genetic import GeneticAlgorithm -------------------------------------------------------------------------------- /pymoode/algorithms/base/differential.py: -------------------------------------------------------------------------------- 1 | # Native 2 | from typing import Optional 3 | 4 | # External 5 | import numpy as np 6 | 7 | # pymoo imports 8 | from pymoo.operators.sampling.lhs import LHS 9 | from pymoo.termination.default import DefaultSingleObjectiveTermination, DefaultMultiObjectiveTermination 10 | from pymoo.util.display.single import SingleObjectiveOutput 11 | from pymoo.util.display.multi import MultiObjectiveOutput 12 | from pymoo.util.misc import has_feasible 13 | 14 | # pymoode imports 15 | from pymoode.operators.variant import DifferentialVariant 16 | from pymoode.algorithms.base.evolutionary import EvolutionaryAlgorithm 17 | from pymoode.survival.replacement import ImprovementReplacement 18 | from pymoode.survival import RankAndCrowding 19 | 20 | 21 | # ========================================================================================================= 22 | # Implementation 23 | # ========================================================================================================= 24 | 25 | class DifferentialEvolution(EvolutionaryAlgorithm): 26 | 27 | def __init__(self, 28 | pop_size=100, 29 | sampling=LHS(), 30 | variant="DE/rand/1/bin", 31 | CR=0.7, 32 | F=(0.5, 1.0), 33 | gamma=1e-4, 34 | de_repair="bounce-back", 35 | survival=ImprovementReplacement(), 36 | advance_after_initial_infill=True, 37 | output=SingleObjectiveOutput(), 38 | **kwargs): 39 | """ 40 | Base class for Differential Evolution algorithms 41 | 42 | Parameters 43 | ---------- 44 | pop_size : int, optional 45 | Population size. Defaults to 100. 46 | 47 | sampling : Sampling, optional 48 | Sampling strategy of pymoo. Defaults to LHS(). 49 | 50 | variant : str, optional 51 | Differential evolution strategy. Must be a string in the format: "DE/selection/n/crossover", in which, n in an integer of number of difference vectors, and crossover is either 'bin' or 'exp'. Selection variants are: 52 | 53 | - 'ranked' 54 | - 'rand' 55 | - 'best' 56 | - 'current-to-best' 57 | - 'current-to-best' 58 | - 'current-to-rand' 59 | - 'rand-to-best' 60 | 61 | The selection strategy 'ranked' might be helpful to improve convergence speed without much harm to diversity. Defaults to 'DE/rand/1/bin'. 62 | 63 | CR : float, optional 64 | Crossover parameter. Defined in the range [0, 1] 65 | To reinforce mutation, use higher values. To control convergence speed, use lower values. 66 | 67 | F : iterable of float or float, optional 68 | Scale factor or mutation parameter. Defined in the range (0, 2] 69 | To reinforce exploration, use higher values; for exploitation, use lower values. 70 | 71 | gamma : float, optional 72 | Jitter deviation parameter. Should be in the range (0, 2). Defaults to 1e-4. 73 | 74 | de_repair : str, optional 75 | Repair of DE mutant vectors. Is either callable or one of: 76 | 77 | - 'bounce-back' 78 | - 'midway' 79 | - 'rand-init' 80 | - 'to-bounds' 81 | 82 | If callable, has the form fun(X, Xb, xl, xu) in which X contains mutated vectors including violations, Xb contains reference vectors for repair in feasible space, xl is a 1d vector of lower bounds, and xu a 1d vector of upper bounds. 83 | Defaults to 'bounce-back'. 84 | 85 | genetic_mutation, optional 86 | Pymoo's genetic mutation operator after crossover. Defaults to NoMutation(). 87 | 88 | survival : Survival, optional 89 | Replacement survival operator. Defaults to ImprovementReplacement(). 90 | 91 | repair : Repair, optional 92 | Pymoo's repair operator after mutation. Defaults to NoRepair(). 93 | """ 94 | 95 | # Mating 96 | mating = DifferentialVariant( 97 | variant=variant, CR=CR, F=F, gamma=gamma, 98 | de_repair=de_repair, **kwargs, 99 | ) 100 | 101 | # Number of offsprings at each generation 102 | n_offsprings = pop_size 103 | 104 | super().__init__( 105 | pop_size=pop_size, 106 | sampling=sampling, 107 | mating=mating, 108 | n_offsprings=n_offsprings, 109 | eliminate_duplicates=None, 110 | survival=survival, 111 | output=output, 112 | advance_after_initial_infill=advance_after_initial_infill, 113 | **kwargs, 114 | ) 115 | 116 | self.termination = DefaultSingleObjectiveTermination() 117 | 118 | def _initialize_advance(self, infills=None, **kwargs): 119 | self.pop = self.survival.do(self.problem, infills, None, n_survive=self.pop_size) 120 | 121 | def _infill(self): 122 | infills = self.mating(self.problem, self.pop, self.n_offsprings, algorithm=self) 123 | return infills 124 | 125 | def _advance(self, infills=None, **kwargs): 126 | 127 | assert infills is not None, "This algorithms uses the AskAndTell interface thus infills must be provided." 128 | 129 | # One-to-one replacement survival 130 | self.pop = self.survival.do(self.problem, self.pop, infills) 131 | 132 | def _set_optimum(self, **kwargs): 133 | if not has_feasible(self.pop): 134 | self.opt = self.pop[[np.argmin(self.pop.get("CV"))]] 135 | else: 136 | self.opt = self.pop[self.pop.get("rank") == 0] 137 | 138 | 139 | class MODE(DifferentialEvolution): 140 | 141 | def __init__(self, 142 | pop_size=100, 143 | sampling=LHS(), 144 | variant="DE/rand/1/bin", 145 | CR=0.5, 146 | F=None, 147 | gamma=0.0001, 148 | de_repair="bounce-back", 149 | survival=RankAndCrowding(), 150 | output=MultiObjectiveOutput(), 151 | **kwargs): 152 | 153 | super().__init__( 154 | pop_size=pop_size, 155 | sampling=sampling, 156 | variant=variant, 157 | CR=CR, 158 | F=F, 159 | gamma=gamma, 160 | de_repair=de_repair, 161 | survival=survival, 162 | output=output, 163 | **kwargs, 164 | ) 165 | 166 | self.termination = DefaultMultiObjectiveTermination() 167 | 168 | def _initialize_advance(self, infills=None, **kwargs): 169 | self.pop = self.survival.do(self.problem, infills, None, n_survive=self.pop_size) 170 | -------------------------------------------------------------------------------- /pymoode/algorithms/base/evolutionary.py: -------------------------------------------------------------------------------- 1 | # Native 2 | from typing import Optional 3 | 4 | # pymoo imports 5 | from pymoo.core.algorithm import Algorithm 6 | from pymoo.core.duplicate import DefaultDuplicateElimination, NoDuplicateElimination 7 | from pymoo.core.initialization import Initialization 8 | from pymoo.core.infill import InfillCriterion 9 | from pymoo.core.population import Population 10 | from pymoo.core.repair import NoRepair, Repair 11 | from pymoo.core.survival import Survival 12 | from pymoo.core.sampling import Sampling 13 | from pymoo.core.mating import Mating 14 | 15 | 16 | # ========================================================================================================= 17 | # Implementation 18 | # ========================================================================================================= 19 | 20 | class EvolutionaryAlgorithm(Algorithm): 21 | 22 | def __init__( 23 | self, 24 | pop_size: Optional[int] = None, 25 | sampling: Optional[Sampling] = None, 26 | mating: Optional[Mating] = None, 27 | survival: Optional[Survival] = None, 28 | n_offsprings: Optional[int] = None, 29 | eliminate_duplicates: bool = True, 30 | repair: Optional[Repair] = None, 31 | advance_after_initial_infill: bool = False, 32 | **kwargs 33 | ): 34 | """Base class for Evolutionary Algorithms 35 | 36 | Parameters 37 | ---------- 38 | pop_size : int, optional 39 | Population size, by default None 40 | 41 | sampling : Sampling, optional 42 | pymoo Sampling instance, by default None 43 | 44 | mating : InfillCriterion, optional 45 | pymoo mating operator, by default None 46 | 47 | survival : Survival, optional 48 | pymoo survival operator, by default None 49 | 50 | n_offsprings : int, optional 51 | Number of offspring individuals created at each generation, by default None 52 | 53 | eliminate_duplicates : DuplicateElimination | bool | None, optional 54 | Eliminate duplicates in mating, by default True 55 | 56 | repair : Repair, optional 57 | pymoo repair operator. In the algorithm level it should be called when sampling. 58 | It is recommended that mating operators also have some repair associated with. By default None 59 | 60 | advance_after_initial_infill : bool, optional 61 | Either or not apply survival after initialization, by default False 62 | """ 63 | 64 | super().__init__(**kwargs) 65 | 66 | # the population size used 67 | self.pop_size = pop_size 68 | 69 | # whether the algorithm should be advanced after initialization of not 70 | self.advance_after_initial_infill = advance_after_initial_infill 71 | 72 | # the survival for the genetic algorithm 73 | self.survival = survival 74 | 75 | # number of offsprings to generate through recombination 76 | self.n_offsprings = n_offsprings 77 | 78 | # if the number of offspring is not set - equal to population size 79 | if self.n_offsprings is None: 80 | self.n_offsprings = pop_size 81 | 82 | # set the duplicate detection class - a boolean value chooses the default duplicate detection 83 | if isinstance(eliminate_duplicates, bool): 84 | if eliminate_duplicates: 85 | self.eliminate_duplicates = DefaultDuplicateElimination() 86 | else: 87 | self.eliminate_duplicates = NoDuplicateElimination() 88 | else: 89 | self.eliminate_duplicates = eliminate_duplicates 90 | 91 | # simply set the no repair object if it is None 92 | self.repair = repair if repair is not None else NoRepair() 93 | 94 | self.initialization = Initialization( 95 | sampling, repair=self.repair, 96 | eliminate_duplicates=self.eliminate_duplicates, 97 | ) 98 | 99 | if mating is None: 100 | mating = InfillCriterion( 101 | repair=self.repair, 102 | eliminate_duplicates=self.eliminate_duplicates, 103 | ) 104 | self.mating = mating 105 | 106 | # other run specific data updated whenever solve is called - to share them in all algorithms 107 | self.n_gen = None 108 | self.pop = None 109 | self.off = None 110 | 111 | def _initialize_infill(self): 112 | pop = self.initialization.do(self.problem, self.pop_size, algorithm=self) 113 | return pop 114 | 115 | def _initialize_advance(self, infills=None, **kwargs): 116 | if self.advance_after_initial_infill: 117 | self.pop = self.survival.do(self.problem, infills, n_survive=len(infills), algorithm=self, **kwargs) 118 | 119 | def _infill(self): 120 | 121 | # do the mating using the current population 122 | off = self.mating.do(self.problem, self.pop, self.n_offsprings, algorithm=self) 123 | 124 | # if the mating could not generate any new offspring (duplicate elimination might make that happen) 125 | if len(off) == 0: 126 | self.termination.force_termination = True 127 | return 128 | 129 | # if not the desired number of offspring could be created 130 | elif len(off) < self.n_offsprings: 131 | if self.verbose: 132 | print("WARNING: Mating could not produce the required number of (unique) offsprings!") 133 | 134 | return off 135 | 136 | def _advance(self, infills=None, **kwargs): 137 | 138 | # the current population 139 | pop = self.pop 140 | 141 | # merge the offsprings with the current population 142 | if infills is not None: 143 | pop = Population.merge(self.pop, infills) 144 | 145 | # execute the survival to find the fittest solutions 146 | self.pop = self.survival.do(self.problem, pop, n_survive=self.pop_size, algorithm=self, **kwargs) 147 | -------------------------------------------------------------------------------- /pymoode/algorithms/base/genetic.py: -------------------------------------------------------------------------------- 1 | # pymoo imports 2 | from pymoode.algorithms.base.evolutionary import EvolutionaryAlgorithm 3 | from pymoo.core.mating import Mating 4 | from pymoo.core.duplicate import DefaultDuplicateElimination, NoDuplicateElimination 5 | 6 | 7 | # ========================================================================================================= 8 | # Implementation 9 | # ========================================================================================================= 10 | 11 | class GeneticAlgorithm(EvolutionaryAlgorithm): 12 | 13 | def __init__(self, 14 | pop_size=None, 15 | sampling=None, 16 | selection=None, 17 | crossover=None, 18 | mutation=None, 19 | survival=None, 20 | n_offsprings=None, 21 | eliminate_duplicates=True, 22 | repair=None, 23 | **kwargs): 24 | """Base class for Genetic Algorithms. A Mating operator is instantiated using 25 | selection, crossover, mutation, repair, and eliminate_duplicates arguments. 26 | 27 | Parameters 28 | ---------- 29 | pop_size : int, optional 30 | Population size, by default None 31 | 32 | sampling : Sampling, optional 33 | pymoo Sampling instance, by default None 34 | 35 | selection : Selection, optional 36 | pymoo parent selection operator, by default None 37 | 38 | crossover : Crossover, optional 39 | pymoo crossover operator, by default None 40 | 41 | genetic_mutation, optional 42 | pymoo mutation operator, by default None 43 | 44 | survival : Survival, optional 45 | pymoo survival operator, by default None 46 | 47 | n_offsprings : int, optional 48 | Number of offspring individuals created at each generation, by default None 49 | 50 | eliminate_duplicates : DuplicateElimination | bool | None, optional 51 | Eliminate duplicates in mating, by default True 52 | 53 | repair : Repair, optional 54 | pymoo repair operator which should be passed to Mating and self. By default None 55 | 56 | advance_after_initial_infill : bool, optional 57 | Either or not apply survival after initialization, by default False 58 | """ 59 | 60 | # set the duplicate detection class - a boolean value chooses the default duplicate detection 61 | if isinstance(eliminate_duplicates, bool): 62 | if eliminate_duplicates: 63 | eliminate_duplicates = DefaultDuplicateElimination() 64 | else: 65 | eliminate_duplicates = NoDuplicateElimination() 66 | 67 | # Mating operator of genetic algorithms 68 | mating = Mating(selection, crossover, mutation, 69 | repair=repair, 70 | eliminate_duplicates=eliminate_duplicates) 71 | 72 | # Instantiate generic evolutionary algorithm 73 | super().__init__( 74 | pop_size=pop_size, 75 | sampling=sampling, 76 | survival=survival, 77 | mating=mating, 78 | n_offsprings=n_offsprings, 79 | eliminate_duplicates=eliminate_duplicates, 80 | repair=repair, 81 | **kwargs, 82 | ) 83 | -------------------------------------------------------------------------------- /pymoode/algorithms/de.py: -------------------------------------------------------------------------------- 1 | # pymoode imports 2 | from pymoode.survival.replacement import ImprovementReplacement 3 | from pymoode.algorithms.base.differential import DifferentialEvolution 4 | 5 | 6 | # ========================================================================================================= 7 | # Implementation 8 | # ========================================================================================================= 9 | 10 | class DE(DifferentialEvolution): 11 | 12 | def __init__(self, 13 | pop_size=100, 14 | variant="DE/rand/1/bin", 15 | CR=0.7, 16 | F=(0.5, 1.0), 17 | gamma=1e-4, 18 | de_repair="bounce-back", 19 | survival=ImprovementReplacement(), 20 | **kwargs): 21 | """ 22 | Single-objective Differential Evolution proposed by Storn and Price (1997). 23 | 24 | Storn, R. & Price, K., 1997. Differential evolution-a simple and efficient heuristic for global optimization over continuous spaces. J. Glob. Optim., 11(4), pp. 341-359. 25 | 26 | Parameters 27 | ---------- 28 | pop_size : int, optional 29 | Population size. Defaults to 100. 30 | 31 | variant : str, optional 32 | Differential evolution strategy. Must be a string in the format: "DE/selection/n/crossover", in which, n in an integer of number of difference vectors, and crossover is either 'bin' or 'exp'. Selection variants are: 33 | 34 | - 'ranked' 35 | - 'rand' 36 | - 'best' 37 | - 'current-to-best' 38 | - 'current-to-best' 39 | - 'current-to-rand' 40 | - 'rand-to-best' 41 | 42 | The selection strategy 'ranked' might be helpful to improve convergence speed without much harm to diversity. Defaults to 'DE/rand/1/bin'. 43 | 44 | CR : float, optional 45 | Crossover parameter. Defined in the range [0, 1] 46 | To reinforce mutation, use higher values. To control convergence speed, use lower values. 47 | 48 | F : iterable of float or float, optional 49 | Scale factor or mutation parameter. Defined in the range (0, 2] 50 | To reinforce exploration, use higher values; for exploitation, use lower values. 51 | 52 | gamma : float, optional 53 | Jitter deviation parameter. Should be in the range (0, 2). Defaults to 1e-4. 54 | 55 | de_repair : str, optional 56 | Repair of DE mutant vectors. Is either callable or one of: 57 | 58 | - 'bounce-back' 59 | - 'midway' 60 | - 'rand-init' 61 | - 'to-bounds' 62 | 63 | If callable, has the form fun(X, Xb, xl, xu) in which X contains mutated vectors including violations, Xb contains reference vectors for repair in feasible space, xl is a 1d vector of lower bounds, and xu a 1d vector of upper bounds. 64 | Defaults to 'bounce-back'. 65 | 66 | genetic_mutation, optional 67 | Pymoo's genetic mutation operator after crossover. Defaults to NoMutation(). 68 | 69 | survival : Survival, optional 70 | Replacement survival operator. Defaults to ImprovementReplacement(). 71 | 72 | repair : Repair, optional 73 | Pymoo's repair operator after mutation. Defaults to NoRepair(). 74 | """ 75 | 76 | super().__init__( 77 | pop_size=pop_size, 78 | variant=variant, 79 | CR=CR, 80 | F=F, 81 | gamma=gamma, 82 | de_repair=de_repair, 83 | survival=survival, 84 | **kwargs, 85 | ) 86 | 87 | -------------------------------------------------------------------------------- /pymoode/algorithms/gde3.py: -------------------------------------------------------------------------------- 1 | # pymoo imports 2 | from pymoo.core.population import Population 3 | from pymoo.util.dominator import get_relation 4 | 5 | # pymoode imports 6 | from pymoode.algorithms.base.differential import MODE 7 | from pymoode.survival import RankAndCrowding 8 | 9 | 10 | # ========================================================================================================= 11 | # Implementation 12 | # ========================================================================================================= 13 | 14 | class GDE3(MODE): 15 | 16 | def __init__(self, 17 | pop_size=100, 18 | variant="DE/rand/1/bin", 19 | CR=0.5, 20 | F=None, 21 | gamma=1e-4, 22 | de_repair="bounce-back", 23 | survival=RankAndCrowding(), 24 | **kwargs): 25 | """ 26 | GDE3 is an extension of DE to multi-objective problems using a mixed type survival strategy. 27 | It is implemented in this version with the same constraint handling strategy of NSGA-II by default. 28 | 29 | Derived algorithms GDE3-MNN and GDE3-2NN use by default survival RankAndCrowding with metrics 'mnn' and '2nn'. 30 | 31 | For many-objective problems, try using NSDE-R, GDE3-MNN, or GDE3-2NN. 32 | 33 | For Bi-objective problems, survival = RankAndCrowding(crowding_func='pcd') is very effective. 34 | 35 | Kukkonen, S. & Lampinen, J., 2005. GDE3: The third evolution step of generalized differential evolution. 2005 IEEE congress on evolutionary computation, Volume 1, pp. 443-450. 36 | 37 | Parameters 38 | ---------- 39 | pop_size : int, optional 40 | Population size. Defaults to 100. 41 | 42 | variant : str, optional 43 | Differential evolution strategy. Must be a string in the format: "DE/selection/n/crossover", in which, n in an integer of number of difference vectors, and crossover is either 'bin' or 'exp'. Selection variants are: 44 | 45 | - 'ranked' 46 | - 'rand' 47 | - 'best' 48 | - 'current-to-best' 49 | - 'current-to-best' 50 | - 'current-to-rand' 51 | - 'rand-to-best' 52 | 53 | The selection strategy 'ranked' might be helpful to improve convergence speed without much harm to diversity. Defaults to 'DE/rand/1/bin'. 54 | 55 | CR : float, optional 56 | Crossover parameter. Defined in the range [0, 1] 57 | To reinforce mutation, use higher values. To control convergence speed, use lower values. 58 | 59 | F : iterable of float or float, optional 60 | Scale factor or mutation parameter. Defined in the range (0, 2] 61 | To reinforce exploration, use higher values; for exploitation, use lower values. 62 | 63 | gamma : float, optional 64 | Jitter deviation parameter. Should be in the range (0, 2). Defaults to 1e-4. 65 | 66 | de_repair : str, optional 67 | Repair of DE mutant vectors. Is either callable or one of: 68 | 69 | - 'bounce-back' 70 | - 'midway' 71 | - 'rand-init' 72 | - 'to-bounds' 73 | 74 | If callable, has the form fun(X, Xb, xl, xu) in which X contains mutated vectors including violations, Xb contains reference vectors for repair in feasible space, xl is a 1d vector of lower bounds, and xu a 1d vector of upper bounds. 75 | Defaults to 'bounce-back'. 76 | 77 | genetic_mutation, optional 78 | Pymoo's genetic mutation operator after crossover. Defaults to NoMutation(). 79 | 80 | repair : Repair, optional 81 | Pymoo's repair operator after mutation. Defaults to NoRepair(). 82 | 83 | survival : Survival, optional 84 | Pymoo's survival strategy. 85 | Defaults to RankAndCrowding() with crowding distances ('cd'). 86 | In GDE3, the survival strategy is applied after a one-to-one comparison between child vector and corresponding parent when both are non-dominated by the other. 87 | """ 88 | 89 | super().__init__( 90 | pop_size=pop_size, 91 | variant=variant, 92 | CR=CR, 93 | F=F, 94 | gamma=gamma, 95 | de_repair=de_repair, 96 | survival=survival, 97 | **kwargs, 98 | ) 99 | 100 | def _advance(self, infills=None, **kwargs): 101 | 102 | assert infills is not None, "This algorithms uses the AskAndTell interface thus 'infills' must to be provided." 103 | 104 | # The individuals that are considered for the survival later and final survive 105 | survivors = [] 106 | 107 | # now for each of the infill solutions 108 | for k in range(len(self.pop)): 109 | 110 | # Get the offspring an the parent it is coming from 111 | off, parent = infills[k], self.pop[k] 112 | 113 | # Check whether the new solution dominates the parent or not 114 | rel = get_relation(parent, off) 115 | 116 | # If indifferent we add both 117 | if rel == 0: 118 | survivors.extend([parent, off]) 119 | 120 | # If offspring dominates parent 121 | elif rel == -1: 122 | survivors.append(off) 123 | 124 | # If parent dominates offspring 125 | else: 126 | survivors.append(parent) 127 | 128 | # Create the population 129 | survivors = Population.create(*survivors) 130 | 131 | # Perform a survival to reduce to pop size 132 | self.pop = self.survival.do(self.problem, survivors, n_survive=self.n_offsprings) 133 | 134 | 135 | class GDE3MNN(GDE3): 136 | 137 | def __init__(self, pop_size=100, variant="DE/rand/1/bin", CR=0.5, F=None, gamma=0.0001, **kwargs): 138 | survival = RankAndCrowding(crowding_func="mnn") 139 | super().__init__(pop_size, variant, CR, F, gamma, survival=survival, **kwargs) 140 | 141 | 142 | class GDE32NN(GDE3): 143 | 144 | def __init__(self, pop_size=100, variant="DE/rand/1/bin", CR=0.5, F=None, gamma=0.0001, **kwargs): 145 | survival = RankAndCrowding(crowding_func="2nn") 146 | super().__init__(pop_size, variant, CR, F, gamma, survival=survival, **kwargs) 147 | 148 | 149 | class GDE3P(GDE3): 150 | 151 | def __init__(self, pop_size=100, variant="DE/rand/1/bin", CR=0.5, F=None, gamma=0.0001, **kwargs): 152 | survival = RankAndCrowding(crowding_func="pcd") 153 | super().__init__(pop_size, variant, CR, F, gamma, survival=survival, **kwargs) 154 | -------------------------------------------------------------------------------- /pymoode/algorithms/nsde.py: -------------------------------------------------------------------------------- 1 | # pymoo imports 2 | from pymoo.core.population import Population 3 | 4 | # pymoode imports 5 | from pymoode.algorithms.base.differential import MODE 6 | from pymoode.survival import RankAndCrowding 7 | 8 | 9 | # ========================================================================================================= 10 | # Implementation 11 | # ========================================================================================================= 12 | 13 | class NSDE(MODE): 14 | 15 | def __init__(self, 16 | pop_size=100, 17 | variant="DE/rand/1/bin", 18 | CR=0.7, 19 | F=None, 20 | gamma=1e-4, 21 | de_repair="bounce-back", 22 | survival=RankAndCrowding(), 23 | **kwargs): 24 | """ 25 | NSDE is an algorithm that combines that combines NSGA-II sorting and survival strategies 26 | to DE mutation and crossover. 27 | 28 | For many-objective problems, try using NSDE-R, GDE3-MNN, or GDE3-2NN. 29 | 30 | For Bi-objective problems, survival = RankAndCrowding(crowding_func='pcd') is very effective. 31 | 32 | Parameters 33 | ---------- 34 | pop_size : int, optional 35 | Population size. Defaults to 100. 36 | 37 | sampling : Sampling, optional 38 | Sampling strategy of pymoo. Defaults to LHS(). 39 | 40 | variant : str, optional 41 | Differential evolution strategy. Must be a string in the format: "DE/selection/n/crossover", in which, n in an integer of number of difference vectors, and crossover is either 'bin' or 'exp'. Selection variants are: 42 | 43 | - "ranked' 44 | - 'rand' 45 | - 'best' 46 | - 'current-to-best' 47 | - 'current-to-best' 48 | - 'current-to-rand' 49 | - 'rand-to-best' 50 | 51 | The selection strategy 'ranked' might be helpful to improve convergence speed without much harm to diversity. Defaults to 'DE/rand/1/bin'. 52 | 53 | CR : float, optional 54 | Crossover parameter. Defined in the range [0, 1] 55 | To reinforce mutation, use higher values. To control convergence speed, use lower values. 56 | 57 | F : iterable of float or float, optional 58 | Scale factor or mutation parameter. Defined in the range (0, 2] 59 | To reinforce exploration, use higher values; for exploitation, use lower values. 60 | 61 | gamma : float, optional 62 | Jitter deviation parameter. Should be in the range (0, 2). Defaults to 1e-4. 63 | 64 | de_repair : str, optional 65 | Repair of DE mutant vectors. Is either callable or one of: 66 | 67 | - 'bounce-back' 68 | - 'midway' 69 | - 'rand-init' 70 | - 'to-bounds' 71 | 72 | If callable, has the form fun(X, Xb, xl, xu) in which X contains mutated vectors including violations, Xb contains reference vectors for repair in feasible space, xl is a 1d vector of lower bounds, and xu a 1d vector of upper bounds. 73 | Defaults to 'bounce-back'. 74 | 75 | genetic_mutation, optional 76 | Pymoo's genetic mutation operator after crossover. Defaults to NoMutation(). 77 | 78 | repair : Repair, optional 79 | Pymoo's repair operator after mutation. Defaults to NoRepair(). 80 | 81 | survival : Survival, optional 82 | Pymoo's survival strategy. 83 | Defaults to RankAndCrowding() with crowding distances ('cd'). 84 | In GDE3, the survival strategy is applied after a one-to-one comparison between child vector and corresponding parent when both are non-dominated by the other. 85 | """ 86 | super().__init__( 87 | pop_size=pop_size, 88 | variant=variant, 89 | CR=CR, 90 | F=F, 91 | gamma=gamma, 92 | de_repair=de_repair, 93 | survival=survival, 94 | **kwargs, 95 | ) 96 | 97 | def _advance(self, infills=None, **kwargs): 98 | 99 | assert infills is not None, "This algorithms uses the AskAndTell interface thus 'infills' must to be provided." 100 | 101 | # Merge in mu + lambda style 102 | pop = Population.merge(self.pop, infills) 103 | 104 | # Perform a survival to reduce to pop size 105 | self.pop = self.survival.do(self.problem, pop, n_survive=self.n_offsprings) 106 | -------------------------------------------------------------------------------- /pymoode/algorithms/nsder.py: -------------------------------------------------------------------------------- 1 | # External 2 | import numpy as np 3 | 4 | # pymoo imports 5 | from pymoo.algorithms.moo.nsga3 import ReferenceDirectionSurvival 6 | from pymoo.util.misc import has_feasible 7 | 8 | # pymoode imports 9 | from pymoode.algorithms.nsde import NSDE 10 | 11 | 12 | # ========================================================================================================= 13 | # Implementation 14 | # ========================================================================================================= 15 | 16 | class NSDER(NSDE): 17 | 18 | def __init__(self, 19 | ref_dirs, 20 | pop_size=100, 21 | variant="DE/rand/1/bin", 22 | CR=0.7, 23 | F=None, 24 | gamma=1e-4, 25 | **kwargs): 26 | """ 27 | NSDE-R is an extension of NSDE to many-objective problems (Reddy & Dulikravich, 2019) using NSGA-III survival. 28 | 29 | S. R. Reddy and G. S. Dulikravich, "Many-objective differential evolution optimization based on reference points: NSDE-R," Struct. Multidisc. Optim., vol. 60, pp. 1455-1473, 2019. 30 | 31 | Parameters 32 | ---------- 33 | ref_dirs : array like 34 | The reference directions that should be used during the optimization. 35 | 36 | pop_size : int, optional 37 | Population size. Defaults to 100. 38 | 39 | sampling : Sampling, optional 40 | Sampling strategy of pymoo. Defaults to LHS(). 41 | 42 | variant : str, optional 43 | Differential evolution strategy. Must be a string in the format: "DE/selection/n/crossover", in which, n in an integer of number of difference vectors, and crossover is either 'bin' or 'exp'. Selection variants are: 44 | 45 | - "ranked' 46 | - 'rand' 47 | - 'best' 48 | - 'current-to-best' 49 | - 'current-to-best' 50 | - 'current-to-rand' 51 | - 'rand-to-best' 52 | 53 | The selection strategy 'ranked' might be helpful to improve convergence speed without much harm to diversity. Defaults to 'DE/rand/1/bin'. 54 | 55 | CR : float, optional 56 | Crossover parameter. Defined in the range [0, 1] 57 | To reinforce mutation, use higher values. To control convergence speed, use lower values. 58 | 59 | F : iterable of float or float, optional 60 | Scale factor or mutation parameter. Defined in the range (0, 2] 61 | To reinforce exploration, use higher values; for exploitation, use lower values. 62 | 63 | gamma : float, optional 64 | Jitter deviation parameter. Should be in the range (0, 2). Defaults to 1e-4. 65 | 66 | de_repair : str, optional 67 | Repair of DE mutant vectors. Is either callable or one of: 68 | 69 | - 'bounce-back' 70 | - 'midway' 71 | - 'rand-init' 72 | - 'to-bounds' 73 | 74 | If callable, has the form fun(X, Xb, xl, xu) in which X contains mutated vectors including violations, Xb contains reference vectors for repair in feasible space, xl is a 1d vector of lower bounds, and xu a 1d vector of upper bounds. 75 | Defaults to 'bounce-back'. 76 | 77 | genetic_mutation, optional 78 | Pymoo's genetic mutation operator after crossover. Defaults to NoMutation(). 79 | 80 | repair : Repair, optional 81 | Pymoo's repair operator after mutation. Defaults to NoRepair(). 82 | 83 | survival : Survival, optional 84 | Pymoo's survival strategy. 85 | Defaults to ReferenceDirectionSurvival(). 86 | """ 87 | 88 | self.ref_dirs = ref_dirs 89 | 90 | if self.ref_dirs is not None: 91 | 92 | if pop_size is None: 93 | pop_size = len(self.ref_dirs) 94 | 95 | if pop_size < len(self.ref_dirs): 96 | print( 97 | f"WARNING: pop_size={pop_size} is less than the number of reference directions ref_dirs={len(self.ref_dirs)}.\n" 98 | "This might cause unwanted behavior of the algorithm. \n" 99 | "Please make sure pop_size is equal or larger than the number of reference directions. ") 100 | 101 | if 'survival' in kwargs: 102 | survival = kwargs['survival'] 103 | del kwargs['survival'] 104 | else: 105 | survival = DERSurvival(ref_dirs) 106 | 107 | super().__init__(pop_size=pop_size, 108 | variant=variant, 109 | CR=CR, 110 | F=F, 111 | gamma=gamma, 112 | survival=survival, 113 | **kwargs) 114 | 115 | def _setup(self, problem, **kwargs): 116 | 117 | if self.ref_dirs is not None: 118 | if self.ref_dirs.shape[1] != problem.n_obj: 119 | raise Exception( 120 | "Dimensionality of reference points must be equal to the number of objectives: %s != %s" % 121 | (self.ref_dirs.shape[1], problem.n_obj)) 122 | 123 | def _set_optimum(self, **kwargs): 124 | if not has_feasible(self.pop): 125 | self.opt = self.pop[[np.argmin(self.pop.get("CV"))]] 126 | else: 127 | self.opt = self.survival.opt 128 | 129 | 130 | class DERSurvival(ReferenceDirectionSurvival): 131 | 132 | def _do(self, problem, pop, *args, n_survive=None, D=None, **kwargs): 133 | return super()._do(problem, pop, n_survive, D, **kwargs) 134 | -------------------------------------------------------------------------------- /pymoode/backups/mnn_bak.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=2, boundscheck=False, wraparound=False, cdivision=True 3 | 4 | import numpy as np 5 | 6 | from libcpp cimport bool 7 | from libcpp.vector cimport vector 8 | from libcpp.set cimport set as cpp_set 9 | 10 | 11 | cdef extern from "math.h": 12 | double HUGE_VAL 13 | 14 | 15 | def calc_mnn(double[:, :] X, int n_remove=0): 16 | 17 | cdef: 18 | int N, M, n 19 | cpp_set[int] extremes 20 | vector[int] extremes_min, extremes_max 21 | 22 | N = X.shape[0] 23 | M = X.shape[1] 24 | 25 | if n_remove <= (N - M): 26 | if n_remove < 0: 27 | n_remove = 0 28 | else: 29 | pass 30 | else: 31 | n_remove = N - M 32 | 33 | extremes_min = c_get_argmin(X) 34 | extremes_max = c_get_argmax(X) 35 | 36 | extremes = cpp_set[int]() 37 | 38 | for n in extremes_min: 39 | extremes.insert(n) 40 | 41 | for n in extremes_max: 42 | extremes.insert(n) 43 | 44 | X = c_normalize_array(X, extremes_max, extremes_min) 45 | 46 | return c_calc_mnn(X, n_remove, N, M, extremes) 47 | 48 | 49 | def calc_2nn(double[:, :] X, int n_remove=0): 50 | 51 | cdef: 52 | int N, M, n 53 | cpp_set[int] extremes 54 | vector[int] extremes_min, extremes_max 55 | 56 | N = X.shape[0] 57 | M = X.shape[1] 58 | 59 | if n_remove <= (N - M): 60 | if n_remove < 0: 61 | n_remove = 0 62 | else: 63 | pass 64 | else: 65 | n_remove = N - M 66 | 67 | extremes_min = c_get_argmin(X) 68 | extremes_max = c_get_argmax(X) 69 | 70 | extremes = cpp_set[int]() 71 | 72 | for n in extremes_min: 73 | extremes.insert(n) 74 | 75 | for n in extremes_max: 76 | extremes.insert(n) 77 | 78 | X = c_normalize_array(X, extremes_max, extremes_min) 79 | 80 | M = 2 81 | 82 | return c_calc_mnn(X, n_remove, N, M, extremes) 83 | 84 | 85 | cdef c_calc_mnn(double[:, :] X, int n_remove, int N, int M, cpp_set[int] extremes): 86 | 87 | cdef: 88 | int n, n_removed, k 89 | cpp_set[int] calc_items 90 | cpp_set[int] H 91 | double[:, :] Dnn, Dij, upper_square 92 | double[:] d 93 | int[:, :] Mnn 94 | 95 | #Define items to calculate distances 96 | calc_items = cpp_set[int]() 97 | for n in range(N): 98 | calc_items.insert(n) 99 | for n in extremes: 100 | calc_items.erase(n) 101 | 102 | #Define remaining items to evaluate 103 | H = cpp_set[int]() 104 | for n in range(N): 105 | H.insert(n) 106 | 107 | #Upper limits for squared l2 distances 108 | _Xsum = np.sum(X, axis=1, keepdims=True) 109 | _upper_square = (_Xsum - _Xsum.T) * (_Xsum - _Xsum.T) / M 110 | upper_square = _upper_square[:, :] 111 | 112 | #Initialize 113 | n_removed = 0 114 | 115 | #Initialize neighbors and distances 116 | _Mnn = np.full((N, M), 0, dtype=np.intc) 117 | _Dnn = np.full((N, M), HUGE_VAL, dtype=np.double) 118 | _Dij = np.full((N, N), -1.0, dtype=np.double) 119 | dd = np.full((N,), HUGE_VAL, dtype=np.double) 120 | 121 | Mnn = _Mnn[:, :] 122 | Dnn = _Dnn[:, :] 123 | Dij = _Dij[:, :] 124 | d = dd[:] 125 | 126 | #Fill in neighbors and distance matrix 127 | c_calc_mnn_iter( 128 | X, 129 | Mnn, 130 | Dnn, 131 | Dij, 132 | N, M, 133 | calc_items, 134 | H, 135 | upper_square 136 | ) 137 | 138 | #Obtain distance metrics 139 | c_calc_d(d, Dnn, calc_items, M) 140 | 141 | #While n_remove not acheived 142 | while n_removed < n_remove: 143 | 144 | #Obtain element to drop 145 | k = c_get_drop(d, H) 146 | H.erase(k) 147 | 148 | #Update index 149 | n_removed = n_removed + 1 150 | if n_removed == n_remove: 151 | break 152 | 153 | else: 154 | 155 | #Get items to be recalculated 156 | calc_items = c_get_calc_items(Mnn, Dnn, H, k, M) 157 | for n in extremes: 158 | calc_items.erase(n) 159 | 160 | #Fill in neighbors and distance matrix 161 | c_calc_mnn_iter( 162 | X, 163 | Mnn, 164 | Dnn, 165 | Dij, 166 | N, M, 167 | calc_items, 168 | H, 169 | upper_square 170 | ) 171 | 172 | #Obtain distance metrics 173 | c_calc_d(d, Dnn, calc_items, M) 174 | 175 | return dd 176 | 177 | 178 | cdef c_calc_mnn_iter( 179 | double[:, :] X, 180 | int[:, :] Mnn, 181 | double[:, :] Dnn, 182 | double[:, :] Dij, 183 | int N, int M, 184 | cpp_set[int] calc_items, 185 | cpp_set[int] H, 186 | double[:, :] upper_square 187 | ): 188 | 189 | cdef: 190 | int i, j, m, MM 191 | double dij 192 | 193 | #MM might be different from M in 2NN 194 | MM = X.shape[1] 195 | 196 | #Iterate over items to calculate 197 | for i in calc_items: 198 | 199 | #Iterate over elements in X 200 | for j in H: 201 | 202 | #Calculate distance if elements satisfy rule 203 | if ((j != i) 204 | and (upper_square[i, j] <= Dnn[i, M-1]) 205 | and (Dij[i, j] <= Dnn[i, M-1])): 206 | 207 | #Calculate if different from -1 208 | if Dij[i, j] == -1.0: 209 | 210 | #Squared distance 211 | dij = 0 212 | for m in range(MM): 213 | dij = dij + (X[j, m] - X[i, m]) * (X[j, m] - X[i, m]) 214 | 215 | #Fill values 216 | Dij[i, j] = dij 217 | Dij[j, i] = Dij[i, j] 218 | 219 | else: 220 | dij = Dij[i, j] 221 | 222 | #Iterate over current values 223 | for m in range(M): 224 | 225 | #Check is any should be replaced 226 | if (dij <= Dnn[i, m]): 227 | 228 | #Replace higher values 229 | Dnn[i, m + 1:] = Dnn[i, m:-1] 230 | Mnn[i, m + 1:] = Mnn[i, m:-1] 231 | 232 | #Replace current value 233 | Dnn[i, m] = dij 234 | Mnn[i, m] = j 235 | 236 | break 237 | 238 | 239 | #Calculate crowding metric 240 | cdef c_calc_d(double[:] d, double[:, :] Dnn, cpp_set[int] calc_items, int M): 241 | 242 | cdef: 243 | int i, m 244 | 245 | for i in calc_items: 246 | 247 | d[i] = 1 248 | for m in range(M): 249 | d[i] = d[i] * Dnn[i, m] 250 | 251 | 252 | #Returns indexes of items to be recalculated after removal 253 | cdef cpp_set[int] c_get_calc_items( 254 | int[:, :] Mnn, 255 | double[:, :] Dnn, 256 | cpp_set[int] H, 257 | int k, int M): 258 | 259 | cdef: 260 | int i, m 261 | cpp_set[int] calc_items 262 | 263 | calc_items = cpp_set[int]() 264 | 265 | for i in H: 266 | 267 | for m in range(M): 268 | 269 | if Mnn[i, m] == k: 270 | 271 | Dnn[i, :] = HUGE_VAL 272 | calc_items.insert(i) 273 | 274 | return calc_items 275 | 276 | 277 | #Returns elements to remove based on crowding metric d and heap of remaining elements H 278 | cdef int c_get_drop(double[:] d, cpp_set[int] H): 279 | 280 | cdef: 281 | int i, min_i 282 | double min_d 283 | 284 | min_d = HUGE_VAL 285 | min_i = 0 286 | 287 | for i in H: 288 | 289 | if d[i] <= min_d: 290 | min_d = d[i] 291 | min_i = i 292 | 293 | return min_i 294 | 295 | 296 | #Returns vector of positions of minimum values along axis 0 of a 2d memoryview 297 | cdef vector[int] c_get_argmin(double[:, :] X): 298 | 299 | cdef: 300 | int N, M, min_i, n, m 301 | double min_val 302 | vector[int] indexes 303 | 304 | N = X.shape[0] 305 | M = X.shape[1] 306 | 307 | indexes = vector[int]() 308 | 309 | for m in range(M): 310 | 311 | min_i = 0 312 | min_val = X[0, m] 313 | 314 | for n in range(N): 315 | 316 | if X[n, m] < min_val: 317 | 318 | min_i = n 319 | min_val = X[n, m] 320 | 321 | indexes.push_back(min_i) 322 | 323 | return indexes 324 | 325 | 326 | #Returns vector of positions of maximum values along axis 0 of a 2d memoryview 327 | cdef vector[int] c_get_argmax(double[:, :] X): 328 | 329 | cdef: 330 | int N, M, max_i, n, m 331 | double max_val 332 | vector[int] indexes 333 | 334 | N = X.shape[0] 335 | M = X.shape[1] 336 | 337 | indexes = vector[int]() 338 | 339 | for m in range(M): 340 | 341 | max_i = 0 342 | max_val = X[0, m] 343 | 344 | for n in range(N): 345 | 346 | if X[n, m] > max_val: 347 | 348 | max_i = n 349 | max_val = X[n, m] 350 | 351 | indexes.push_back(max_i) 352 | 353 | return indexes 354 | 355 | 356 | #Performs normalization of a 2d memoryview 357 | cdef double[:, :] c_normalize_array(double[:, :] X, vector[int] extremes_max, vector[int] extremes_min): 358 | 359 | cdef: 360 | int N = X.shape[0] 361 | int M = X.shape[1] 362 | int n, m, l, u 363 | double l_val, u_val, diff_val 364 | vector[double] min_vals, max_vals 365 | 366 | min_vals = vector[double]() 367 | max_vals = vector[double]() 368 | 369 | m = 0 370 | for u in extremes_max: 371 | u_val = X[u, m] 372 | max_vals.push_back(u_val) 373 | m = m + 1 374 | 375 | m = 0 376 | for l in extremes_min: 377 | l_val = X[l, m] 378 | min_vals.push_back(l_val) 379 | m = m + 1 380 | 381 | for m in range(M): 382 | 383 | diff_val = max_vals[m] - min_vals[m] 384 | if diff_val == 0.0: 385 | diff_val = 1.0 386 | 387 | for n in range(N): 388 | 389 | X[n, m] = (X[n, m] - min_vals[m]) / diff_val 390 | 391 | return X -------------------------------------------------------------------------------- /pymoode/backups/mnn_naive_sort.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=2, boundscheck=False, wraparound=False, cdivision=True 3 | 4 | # This was implemented using a naive sorting approach and calculating the full distances matrix 5 | # Other strategies can be more efficient depending on the population size and number of objectives 6 | # This approach was the most promising for N = 3 and a population size < 30 7 | # For population sizes > 30 using numpy quicksort implementation would make the algorithm faster 8 | # I believe for a large number of objectives M, some strategy based on upper bounds for distances would be helpful 9 | # Those interested in contributing please contact me at bruscalia12@gmail.com 10 | 11 | 12 | import numpy as np 13 | 14 | from libcpp cimport bool 15 | from libcpp.vector cimport vector 16 | from libcpp.set cimport set as cpp_set 17 | 18 | 19 | cdef extern from "math.h": 20 | double HUGE_VAL 21 | 22 | 23 | def calc_mnn(double[:, :] X, int n_remove=0): 24 | 25 | cdef: 26 | int N, M, n 27 | cpp_set[int] extremes 28 | vector[int] extremes_min, extremes_max 29 | 30 | N = X.shape[0] 31 | M = X.shape[1] 32 | 33 | if n_remove <= (N - M): 34 | if n_remove < 0: 35 | n_remove = 0 36 | else: 37 | pass 38 | else: 39 | n_remove = N - M 40 | 41 | extremes_min = c_get_argmin(X) 42 | extremes_max = c_get_argmax(X) 43 | 44 | extremes = cpp_set[int]() 45 | 46 | for n in extremes_min: 47 | extremes.insert(n) 48 | 49 | for n in extremes_max: 50 | extremes.insert(n) 51 | 52 | X = c_normalize_array(X, extremes_max, extremes_min) 53 | 54 | return c_calc_mnn(X, n_remove, N, M, extremes) 55 | 56 | 57 | def calc_2nn(double[:, :] X, int n_remove=0): 58 | 59 | cdef: 60 | int N, M, n 61 | cpp_set[int] extremes 62 | vector[int] extremes_min, extremes_max 63 | 64 | N = X.shape[0] 65 | M = X.shape[1] 66 | 67 | if n_remove <= (N - M): 68 | if n_remove < 0: 69 | n_remove = 0 70 | else: 71 | pass 72 | else: 73 | n_remove = N - M 74 | 75 | extremes_min = c_get_argmin(X) 76 | extremes_max = c_get_argmax(X) 77 | 78 | extremes = cpp_set[int]() 79 | 80 | for n in extremes_min: 81 | extremes.insert(n) 82 | 83 | for n in extremes_max: 84 | extremes.insert(n) 85 | 86 | X = c_normalize_array(X, extremes_max, extremes_min) 87 | 88 | M = 2 89 | 90 | return c_calc_mnn(X, n_remove, N, M, extremes) 91 | 92 | 93 | cdef c_calc_mnn(double[:, :] X, int n_remove, int N, int M, cpp_set[int] extremes): 94 | 95 | cdef: 96 | int n, mm, i, j, n_removed, k, MM 97 | double dij 98 | cpp_set[int] calc_items 99 | cpp_set[int] H 100 | double[:, :] D 101 | double[:] d 102 | int[:, :] Mnn 103 | 104 | #Define items to calculate distances 105 | calc_items = cpp_set[int]() 106 | for n in range(N): 107 | calc_items.insert(n) 108 | for n in extremes: 109 | calc_items.erase(n) 110 | 111 | #Define remaining items to evaluate 112 | H = cpp_set[int]() 113 | for n in range(N): 114 | H.insert(n) 115 | 116 | #Instantiate distances array 117 | _D = np.empty((N, N), dtype=np.double) 118 | D = _D[:, :] 119 | 120 | #Shape of X 121 | MM = X.shape[1] 122 | 123 | #Fill values on D 124 | for i in range(N - 1): 125 | D[i, i] = 0.0 126 | 127 | for j in range(i + 1, N): 128 | 129 | dij = 0 130 | for mm in range(MM): 131 | dij = dij + (X[j, mm] - X[i, mm]) * (X[j, mm] - X[i, mm]) 132 | 133 | D[i, j] = dij 134 | D[j, i] = D[i, j] 135 | 136 | D[N-1, N-1] = 0.0 137 | 138 | #Initialize 139 | n_removed = 0 140 | 141 | #Initialize neighbors and distances 142 | _Mnn = np.full((N, M), -1, dtype=np.intc) 143 | dd = np.full((N,), HUGE_VAL, dtype=np.double) 144 | 145 | Mnn = _Mnn[:, :] 146 | d = dd[:] 147 | 148 | #Fill in neighbors and distance matrix 149 | c_calc_mnn_iter( 150 | X, 151 | Mnn, 152 | D, 153 | N, M, 154 | calc_items, 155 | H 156 | ) 157 | 158 | #Obtain distance metrics 159 | c_calc_d(d, Mnn, D, calc_items, M) 160 | 161 | #While n_remove not acheived (no need to recalculate if only one item should be removed) 162 | while n_removed < (n_remove - 1): 163 | 164 | #Obtain element to drop 165 | k = c_get_drop(d, H) 166 | H.erase(k) 167 | 168 | #Update index 169 | n_removed = n_removed + 1 170 | 171 | #Get items to be recalculated 172 | calc_items = c_get_calc_items(Mnn, H, k, M) 173 | for n in extremes: 174 | calc_items.erase(n) 175 | 176 | #Fill in neighbors and distance matrix 177 | c_calc_mnn_iter( 178 | X, 179 | Mnn, 180 | D, 181 | N, M, 182 | calc_items, 183 | H 184 | ) 185 | 186 | #Obtain distance metrics 187 | c_calc_d(d, Mnn, D, calc_items, M) 188 | 189 | return dd 190 | 191 | 192 | cdef c_calc_mnn_iter( 193 | double[:, :] X, 194 | int[:, :] Mnn, 195 | double[:, :] D, 196 | int N, int M, 197 | cpp_set[int] calc_items, 198 | cpp_set[int] H 199 | ): 200 | 201 | cdef: 202 | int i, j, m 203 | 204 | #Iterate over items to calculate 205 | for i in calc_items: 206 | 207 | #Iterate over elements in X 208 | for j in H: 209 | 210 | #Go to next if same element 211 | if (j == i): 212 | continue 213 | 214 | #Replace at least the last neighbor 215 | elif (D[i, j] <= D[i, Mnn[i, M-1]]) or (Mnn[i, M-1] == -1): 216 | 217 | #Iterate over current values 218 | for m in range(M): 219 | 220 | #Set to current if unassigned 221 | if (Mnn[i, m] == -1): 222 | 223 | #Set last neighbor to index 224 | Mnn[i, m] = j 225 | break 226 | 227 | #Break if checking already corresponding index 228 | elif (j == Mnn[i, m]): 229 | break 230 | 231 | #Distance satisfies condition 232 | elif (D[i, j] <= D[i, Mnn[i, m]]): 233 | 234 | #Replace higher values 235 | Mnn[i, m + 1:] = Mnn[i, m:-1] 236 | 237 | #Replace current value 238 | Mnn[i, m] = j 239 | break 240 | 241 | 242 | #Calculate crowding metric 243 | cdef c_calc_d(double[:] d, int[:, :] Mnn, double[:, :] D, cpp_set[int] calc_items, int M): 244 | 245 | cdef: 246 | int i, m 247 | 248 | for i in calc_items: 249 | 250 | d[i] = 1 251 | for m in range(M): 252 | d[i] = d[i] * D[i, Mnn[i, m]] 253 | 254 | 255 | #Returns indexes of items to be recalculated after removal 256 | cdef cpp_set[int] c_get_calc_items( 257 | int[:, :] Mnn, 258 | cpp_set[int] H, 259 | int k, int M): 260 | 261 | cdef: 262 | int i, m 263 | cpp_set[int] calc_items 264 | 265 | calc_items = cpp_set[int]() 266 | 267 | for i in H: 268 | 269 | for m in range(M): 270 | 271 | if Mnn[i, m] == k: 272 | 273 | Mnn[i, m:-1] = Mnn[i, m + 1:] 274 | Mnn[i, M-1] = -1 275 | 276 | calc_items.insert(i) 277 | 278 | return calc_items 279 | 280 | 281 | #Returns elements to remove based on crowding metric d and heap of remaining elements H 282 | cdef int c_get_drop(double[:] d, cpp_set[int] H): 283 | 284 | cdef: 285 | int i, min_i 286 | double min_d 287 | 288 | min_d = HUGE_VAL 289 | min_i = 0 290 | 291 | for i in H: 292 | 293 | if d[i] <= min_d: 294 | min_d = d[i] 295 | min_i = i 296 | 297 | return min_i 298 | 299 | 300 | #Elements in condensed matrix 301 | cdef int c_square_to_condensed(int i, int j, int N): 302 | 303 | cdef int _i = i 304 | 305 | if i < j: 306 | i = j 307 | j = _i 308 | 309 | return N * j - j * (j + 1) // 2 + i - 1 - j 310 | 311 | 312 | #Returns vector of positions of minimum values along axis 0 of a 2d memoryview 313 | cdef vector[int] c_get_argmin(double[:, :] X): 314 | 315 | cdef: 316 | int N, M, min_i, n, m 317 | double min_val 318 | vector[int] indexes 319 | 320 | N = X.shape[0] 321 | M = X.shape[1] 322 | 323 | indexes = vector[int]() 324 | 325 | for m in range(M): 326 | 327 | min_i = 0 328 | min_val = X[0, m] 329 | 330 | for n in range(N): 331 | 332 | if X[n, m] < min_val: 333 | 334 | min_i = n 335 | min_val = X[n, m] 336 | 337 | indexes.push_back(min_i) 338 | 339 | return indexes 340 | 341 | 342 | #Returns vector of positions of maximum values along axis 0 of a 2d memoryview 343 | cdef vector[int] c_get_argmax(double[:, :] X): 344 | 345 | cdef: 346 | int N, M, max_i, n, m 347 | double max_val 348 | vector[int] indexes 349 | 350 | N = X.shape[0] 351 | M = X.shape[1] 352 | 353 | indexes = vector[int]() 354 | 355 | for m in range(M): 356 | 357 | max_i = 0 358 | max_val = X[0, m] 359 | 360 | for n in range(N): 361 | 362 | if X[n, m] > max_val: 363 | 364 | max_i = n 365 | max_val = X[n, m] 366 | 367 | indexes.push_back(max_i) 368 | 369 | return indexes 370 | 371 | 372 | #Performs normalization of a 2d memoryview 373 | cdef double[:, :] c_normalize_array(double[:, :] X, vector[int] extremes_max, vector[int] extremes_min): 374 | 375 | cdef: 376 | int N = X.shape[0] 377 | int M = X.shape[1] 378 | int n, m, l, u 379 | double l_val, u_val, diff_val 380 | vector[double] min_vals, max_vals 381 | 382 | min_vals = vector[double]() 383 | max_vals = vector[double]() 384 | 385 | m = 0 386 | for u in extremes_max: 387 | u_val = X[u, m] 388 | max_vals.push_back(u_val) 389 | m = m + 1 390 | 391 | m = 0 392 | for l in extremes_min: 393 | l_val = X[l, m] 394 | min_vals.push_back(l_val) 395 | m = m + 1 396 | 397 | for m in range(M): 398 | 399 | diff_val = max_vals[m] - min_vals[m] 400 | if diff_val == 0.0: 401 | diff_val = 1.0 402 | 403 | for n in range(N): 404 | 405 | X[n, m] = (X[n, m] - min_vals[m]) / diff_val 406 | 407 | return X -------------------------------------------------------------------------------- /pymoode/backups/mnn_precalc.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=2, boundscheck=False, wraparound=False, cdivision=True 3 | 4 | import numpy as np 5 | from scipy.spatial.distance import squareform, pdist 6 | 7 | from libcpp cimport bool 8 | from libcpp.vector cimport vector 9 | from libcpp.set cimport set as cpp_set 10 | 11 | 12 | cdef extern from "math.h": 13 | double HUGE_VAL 14 | 15 | 16 | def calc_mnn(double[:, :] X, int n_remove=0): 17 | 18 | cdef: 19 | int N, M, n 20 | cpp_set[int] extremes 21 | vector[int] extremes_min, extremes_max 22 | 23 | N = X.shape[0] 24 | M = X.shape[1] 25 | 26 | if n_remove <= (N - M): 27 | if n_remove < 0: 28 | n_remove = 0 29 | else: 30 | pass 31 | else: 32 | n_remove = N - M 33 | 34 | extremes_min = c_get_argmin(X) 35 | extremes_max = c_get_argmax(X) 36 | 37 | extremes = cpp_set[int]() 38 | 39 | for n in extremes_min: 40 | extremes.insert(n) 41 | 42 | for n in extremes_max: 43 | extremes.insert(n) 44 | 45 | X = c_normalize_array(X, extremes_max, extremes_min) 46 | 47 | return c_calc_mnn(X, n_remove, N, M, extremes) 48 | 49 | 50 | def calc_2nn(double[:, :] X, int n_remove=0): 51 | 52 | cdef: 53 | int N, M, n 54 | cpp_set[int] extremes 55 | vector[int] extremes_min, extremes_max 56 | 57 | N = X.shape[0] 58 | M = X.shape[1] 59 | 60 | if n_remove <= (N - M): 61 | if n_remove < 0: 62 | n_remove = 0 63 | else: 64 | pass 65 | else: 66 | n_remove = N - M 67 | 68 | extremes_min = c_get_argmin(X) 69 | extremes_max = c_get_argmax(X) 70 | 71 | extremes = cpp_set[int]() 72 | 73 | for n in extremes_min: 74 | extremes.insert(n) 75 | 76 | for n in extremes_max: 77 | extremes.insert(n) 78 | 79 | X = c_normalize_array(X, extremes_max, extremes_min) 80 | 81 | M = 2 82 | 83 | return c_calc_mnn(X, n_remove, N, M, extremes) 84 | 85 | 86 | cdef c_calc_mnn(double[:, :] X, int n_remove, int N, int M, cpp_set[int] extremes): 87 | 88 | cdef: 89 | int n, n_removed, k 90 | cpp_set[int] calc_items 91 | cpp_set[int] H 92 | double[:, :] D 93 | double[:] d 94 | int[:, :] Mnn 95 | 96 | #Define items to calculate distances 97 | calc_items = cpp_set[int]() 98 | for n in range(N): 99 | calc_items.insert(n) 100 | for n in extremes: 101 | calc_items.erase(n) 102 | 103 | #Define remaining items to evaluate 104 | H = cpp_set[int]() 105 | for n in range(N): 106 | H.insert(n) 107 | 108 | #Initialize 109 | n_removed = 0 110 | 111 | #Initialize neighbors and distances 112 | _Mnn = np.full((N, M), -1, dtype=np.intc) 113 | _D = squareform(pdist(X, metric="sqeuclidean").astype(np.double)) 114 | dd = np.full((N,), HUGE_VAL, dtype=np.double) 115 | 116 | Mnn = _Mnn[:, :] 117 | D = _D[:, :] 118 | d = dd[:] 119 | 120 | #Fill in neighbors and distance matrix 121 | c_calc_mnn_iter( 122 | X, 123 | Mnn, 124 | D, 125 | N, M, 126 | calc_items, 127 | H 128 | ) 129 | 130 | #Obtain distance metrics 131 | c_calc_d(d, Mnn, D, calc_items, M) 132 | 133 | #While n_remove not acheived 134 | while n_removed < n_remove: 135 | 136 | #Obtain element to drop 137 | k = c_get_drop(d, H) 138 | H.erase(k) 139 | 140 | #Update index 141 | n_removed = n_removed + 1 142 | if n_removed == n_remove: 143 | break 144 | 145 | else: 146 | 147 | #Get items to be recalculated 148 | calc_items = c_get_calc_items(Mnn, D, H, k, M) 149 | for n in extremes: 150 | calc_items.erase(n) 151 | 152 | #Fill in neighbors and distance matrix 153 | c_calc_mnn_iter( 154 | X, 155 | Mnn, 156 | D, 157 | N, M, 158 | calc_items, 159 | H 160 | ) 161 | 162 | #Obtain distance metrics 163 | c_calc_d(d, Mnn, D, calc_items, M) 164 | 165 | return dd 166 | 167 | 168 | cdef c_calc_mnn_iter( 169 | double[:, :] X, 170 | int[:, :] Mnn, 171 | double[:, :] D, 172 | int N, int M, 173 | cpp_set[int] calc_items, 174 | cpp_set[int] H 175 | ): 176 | 177 | cdef: 178 | int i, j, m, last_n, MM 179 | double dij, last_d, dim 180 | 181 | #MM might be different from M in 2NN 182 | MM = X.shape[1] 183 | 184 | #Iterate over items to calculate 185 | for i in calc_items: 186 | 187 | #Set last neighbor to index 188 | last_n = Mnn[i, M-1] 189 | 190 | #Set to huge val if unassinged 191 | if last_n == -1: 192 | last_d = HUGE_VAL 193 | else: 194 | last_d = D[i, last_n] 195 | 196 | #Iterate over elements in X 197 | for j in H: 198 | 199 | #Calculate distance if elements satisfy rule 200 | if ((j != i) and (j != last_n) and (D[i, j] <= last_d)): 201 | 202 | #Iterate over current values 203 | for m in range(M): 204 | 205 | #Break if checking already corresponding index 206 | if (j == Mnn[i, m]): 207 | break 208 | 209 | #Set to current if unassigned 210 | elif (Mnn[i, m] == -1): 211 | 212 | #Set last neighbor to index 213 | Mnn[i, m] = j 214 | last_n = Mnn[i, M-1] 215 | 216 | #Set to huge val if unassinged 217 | if last_n == -1: 218 | last_d = HUGE_VAL 219 | else: 220 | last_d = D[i, last_n] 221 | break 222 | 223 | elif (D[i, j] <= D[i, Mnn[i, m]]): 224 | 225 | #Replace higher values 226 | Mnn[i, m + 1:] = Mnn[i, m:-1] 227 | 228 | #Replace current value 229 | Mnn[i, m] = j 230 | 231 | #Set last neighbor to index 232 | last_n = Mnn[i, M-1] 233 | 234 | #Set to huge val if unassinged 235 | if last_n == -1: 236 | last_d = HUGE_VAL 237 | else: 238 | last_d = D[i, last_n] 239 | break 240 | 241 | 242 | #Calculate crowding metric 243 | cdef c_calc_d(double[:] d, int[:, :] Mnn, double[:, :] D, cpp_set[int] calc_items, int M): 244 | 245 | cdef: 246 | int i, m 247 | 248 | for i in calc_items: 249 | 250 | d[i] = 1 251 | for m in range(M): 252 | d[i] = d[i] * D[i, Mnn[i, m]] 253 | 254 | 255 | #Returns indexes of items to be recalculated after removal 256 | cdef cpp_set[int] c_get_calc_items( 257 | int[:, :] Mnn, 258 | double[:, :] D, 259 | cpp_set[int] H, 260 | int k, int M): 261 | 262 | cdef: 263 | int i, m 264 | cpp_set[int] calc_items 265 | 266 | calc_items = cpp_set[int]() 267 | 268 | for i in H: 269 | 270 | for m in range(M): 271 | 272 | if Mnn[i, m] == k: 273 | 274 | Mnn[i, m:-1] = Mnn[i, m + 1:] 275 | Mnn[i, M-1] = -1 276 | 277 | calc_items.insert(i) 278 | 279 | return calc_items 280 | 281 | 282 | #Returns elements to remove based on crowding metric d and heap of remaining elements H 283 | cdef int c_get_drop(double[:] d, cpp_set[int] H): 284 | 285 | cdef: 286 | int i, min_i 287 | double min_d 288 | 289 | min_d = HUGE_VAL 290 | min_i = 0 291 | 292 | for i in H: 293 | 294 | if d[i] <= min_d: 295 | min_d = d[i] 296 | min_i = i 297 | 298 | return min_i 299 | 300 | 301 | #Returns vector of positions of minimum values along axis 0 of a 2d memoryview 302 | cdef vector[int] c_get_argmin(double[:, :] X): 303 | 304 | cdef: 305 | int N, M, min_i, n, m 306 | double min_val 307 | vector[int] indexes 308 | 309 | N = X.shape[0] 310 | M = X.shape[1] 311 | 312 | indexes = vector[int]() 313 | 314 | for m in range(M): 315 | 316 | min_i = 0 317 | min_val = X[0, m] 318 | 319 | for n in range(N): 320 | 321 | if X[n, m] < min_val: 322 | 323 | min_i = n 324 | min_val = X[n, m] 325 | 326 | indexes.push_back(min_i) 327 | 328 | return indexes 329 | 330 | 331 | #Returns vector of positions of maximum values along axis 0 of a 2d memoryview 332 | cdef vector[int] c_get_argmax(double[:, :] X): 333 | 334 | cdef: 335 | int N, M, max_i, n, m 336 | double max_val 337 | vector[int] indexes 338 | 339 | N = X.shape[0] 340 | M = X.shape[1] 341 | 342 | indexes = vector[int]() 343 | 344 | for m in range(M): 345 | 346 | max_i = 0 347 | max_val = X[0, m] 348 | 349 | for n in range(N): 350 | 351 | if X[n, m] > max_val: 352 | 353 | max_i = n 354 | max_val = X[n, m] 355 | 356 | indexes.push_back(max_i) 357 | 358 | return indexes 359 | 360 | 361 | #Performs normalization of a 2d memoryview 362 | cdef double[:, :] c_normalize_array(double[:, :] X, vector[int] extremes_max, vector[int] extremes_min): 363 | 364 | cdef: 365 | int N = X.shape[0] 366 | int M = X.shape[1] 367 | int n, m, l, u 368 | double l_val, u_val, diff_val 369 | vector[double] min_vals, max_vals 370 | 371 | min_vals = vector[double]() 372 | max_vals = vector[double]() 373 | 374 | m = 0 375 | for u in extremes_max: 376 | u_val = X[u, m] 377 | max_vals.push_back(u_val) 378 | m = m + 1 379 | 380 | m = 0 381 | for l in extremes_min: 382 | l_val = X[l, m] 383 | min_vals.push_back(l_val) 384 | m = m + 1 385 | 386 | for m in range(M): 387 | 388 | diff_val = max_vals[m] - min_vals[m] 389 | if diff_val == 0.0: 390 | diff_val = 1.0 391 | 392 | for n in range(N): 393 | 394 | X[n, m] = (X[n, m] - min_vals[m]) / diff_val 395 | 396 | return X -------------------------------------------------------------------------------- /pymoode/cython/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/pymoode/cython/__init__.py -------------------------------------------------------------------------------- /pymoode/cython/info.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=2, boundscheck=False, wraparound=False, cdivision=True 3 | 4 | def info(): 5 | return "This file was based on pymoo to check compilation success" -------------------------------------------------------------------------------- /pymoode/cython/mnn.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=2, boundscheck=False, wraparound=False, cdivision=True 3 | 4 | # This was implemented using the full distances matrix 5 | # Other strategies can be more efficient depending on the population size and number of objectives 6 | # This approach was the most promising for N = 3 7 | # I believe for a large number of objectives M, some strategy based on upper bounds for distances would be helpful 8 | # Those interested in contributing please contact me at bruscalia12@gmail.com 9 | 10 | 11 | import numpy as np 12 | 13 | from pymoode.cython.utils cimport c_get_drop, c_get_argmin, c_get_argmax, c_normalize_array 14 | 15 | from libcpp cimport bool 16 | from libcpp.vector cimport vector 17 | from libcpp.set cimport set as cpp_set 18 | 19 | 20 | cdef extern from "math.h": 21 | double HUGE_VAL 22 | 23 | 24 | def calc_mnn(double[:, :] X, int n_remove=0): 25 | 26 | cdef: 27 | int N, M, n 28 | cpp_set[int] extremes 29 | vector[int] extremes_min, extremes_max 30 | 31 | N = X.shape[0] 32 | M = X.shape[1] 33 | 34 | if n_remove <= (N - M): 35 | if n_remove < 0: 36 | n_remove = 0 37 | else: 38 | n_remove = N - M 39 | 40 | extremes_min = c_get_argmin(X) 41 | extremes_max = c_get_argmax(X) 42 | 43 | extremes = cpp_set[int]() 44 | 45 | for n in extremes_min: 46 | extremes.insert(n) 47 | 48 | for n in extremes_max: 49 | extremes.insert(n) 50 | 51 | X = c_normalize_array(X, extremes_max, extremes_min) 52 | 53 | return c_calc_mnn(X, n_remove, N, M, extremes) 54 | 55 | 56 | def calc_2nn(double[:, :] X, int n_remove=0): 57 | 58 | cdef: 59 | int N, M, n 60 | cpp_set[int] extremes 61 | vector[int] extremes_min, extremes_max 62 | 63 | N = X.shape[0] 64 | M = X.shape[1] 65 | 66 | if n_remove <= (N - M): 67 | if n_remove < 0: 68 | n_remove = 0 69 | else: 70 | n_remove = N - M 71 | 72 | extremes_min = c_get_argmin(X) 73 | extremes_max = c_get_argmax(X) 74 | 75 | extremes = cpp_set[int]() 76 | 77 | for n in extremes_min: 78 | extremes.insert(n) 79 | 80 | for n in extremes_max: 81 | extremes.insert(n) 82 | 83 | X = c_normalize_array(X, extremes_max, extremes_min) 84 | 85 | M = 2 86 | 87 | return c_calc_mnn(X, n_remove, N, M, extremes) 88 | 89 | 90 | cdef double[:] c_calc_mnn(double[:, :] X, int n_remove, int N, int M, cpp_set[int] extremes): 91 | 92 | cdef: 93 | int n, mm, i, j, n_removed, k, MM 94 | double dij 95 | cpp_set[int] calc_items 96 | cpp_set[int] H 97 | double[:, :] D 98 | double[:] d 99 | int[:, :] Mnn 100 | 101 | # Return infinity if not enough points 102 | if N <= M: 103 | return np.full(N, HUGE_VAL, dtype=np.double) 104 | 105 | # Define items to calculate distances 106 | calc_items = cpp_set[int]() 107 | for n in range(N): 108 | calc_items.insert(n) 109 | for n in extremes: 110 | calc_items.erase(n) 111 | 112 | # Define remaining items to evaluate 113 | H = cpp_set[int]() 114 | for n in range(N): 115 | H.insert(n) 116 | 117 | # Instantiate distances array 118 | _D = np.empty((N, N), dtype=np.double) 119 | D = _D[:, :] 120 | 121 | # Shape of X 122 | MM = X.shape[1] 123 | 124 | # Fill values on D 125 | for i in range(N - 1): 126 | D[i, i] = 0.0 127 | 128 | for j in range(i + 1, N): 129 | 130 | dij = 0 131 | for mm in range(MM): 132 | dij = dij + (X[j, mm] - X[i, mm]) * (X[j, mm] - X[i, mm]) 133 | 134 | D[i, j] = dij 135 | D[j, i] = D[i, j] 136 | 137 | D[N-1, N-1] = 0.0 138 | 139 | # Initialize 140 | n_removed = 0 141 | 142 | # Initialize neighbors and distances 143 | # _Mnn = np.full((N, M), -1, dtype=np.intc) 144 | _Mnn = np.argpartition(D, range(1, M+1), axis=1)[:, 1:M+1].astype(np.intc) 145 | dd = np.full((N,), HUGE_VAL, dtype=np.double) 146 | 147 | Mnn = _Mnn[:, :] 148 | d = dd[:] 149 | 150 | # Obtain distance metrics 151 | c_calc_d(d, Mnn, D, calc_items, M) 152 | 153 | # While n_remove not acheived (no need to recalculate if only one item should be removed) 154 | while n_removed < (n_remove - 1): 155 | 156 | # Obtain element to drop 157 | k = c_get_drop(d, H) 158 | H.erase(k) 159 | 160 | # Update index 161 | n_removed = n_removed + 1 162 | 163 | # Get items to be recalculated 164 | calc_items = c_get_calc_items(Mnn, H, k, M) 165 | for n in extremes: 166 | calc_items.erase(n) 167 | 168 | # Fill in neighbors and distance matrix 169 | c_calc_mnn_iter( 170 | X, 171 | Mnn, 172 | D, 173 | N, M, 174 | calc_items, 175 | H 176 | ) 177 | 178 | # Obtain distance metrics 179 | c_calc_d(d, Mnn, D, calc_items, M) 180 | 181 | return dd 182 | 183 | 184 | cdef c_calc_mnn_iter( 185 | double[:, :] X, 186 | int[:, :] Mnn, 187 | double[:, :] D, 188 | int N, int M, 189 | cpp_set[int] calc_items, 190 | cpp_set[int] H 191 | ): 192 | 193 | cdef: 194 | int i, j, m 195 | 196 | # Iterate over items to calculate 197 | for i in calc_items: 198 | 199 | # Iterate over elements in X 200 | for j in H: 201 | 202 | # Go to next if same element 203 | if (j == i): 204 | continue 205 | 206 | # Replace at least the last neighbor 207 | elif (D[i, j] <= D[i, Mnn[i, M-1]]) or (Mnn[i, M-1] == -1): 208 | 209 | # Iterate over current values 210 | for m in range(M): 211 | 212 | # Set to current if unassigned 213 | if (Mnn[i, m] == -1): 214 | 215 | # Set last neighbor to index 216 | Mnn[i, m] = j 217 | break 218 | 219 | # Break if checking already corresponding index 220 | elif (j == Mnn[i, m]): 221 | break 222 | 223 | # Distance satisfies condition 224 | elif (D[i, j] <= D[i, Mnn[i, m]]): 225 | 226 | # Replace higher values 227 | Mnn[i, m + 1:] = Mnn[i, m:-1] 228 | 229 | # Replace current value 230 | Mnn[i, m] = j 231 | break 232 | 233 | 234 | # Calculate crowding metric 235 | cdef c_calc_d(double[:] d, int[:, :] Mnn, double[:, :] D, cpp_set[int] calc_items, int M): 236 | 237 | cdef: 238 | int i, m 239 | 240 | for i in calc_items: 241 | 242 | d[i] = 1 243 | for m in range(M): 244 | d[i] = d[i] * D[i, Mnn[i, m]] 245 | 246 | 247 | # Returns indexes of items to be recalculated after removal 248 | cdef cpp_set[int] c_get_calc_items( 249 | int[:, :] Mnn, 250 | cpp_set[int] H, 251 | int k, int M): 252 | 253 | cdef: 254 | int i, m 255 | cpp_set[int] calc_items 256 | 257 | calc_items = cpp_set[int]() 258 | 259 | for i in H: 260 | 261 | for m in range(M): 262 | 263 | if Mnn[i, m] == k: 264 | 265 | Mnn[i, m:-1] = Mnn[i, m + 1:] 266 | Mnn[i, M-1] = -1 267 | 268 | calc_items.insert(i) 269 | 270 | return calc_items 271 | -------------------------------------------------------------------------------- /pymoode/cython/pruning_cd.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=2, boundscheck=False, wraparound=False, cdivision=True 3 | 4 | import numpy as np 5 | 6 | from pymoode.cython.utils cimport c_get_drop, c_get_argmin, c_get_argmax, c_normalize_array 7 | 8 | from libcpp cimport bool 9 | from libcpp.vector cimport vector 10 | from libcpp.set cimport set as cpp_set 11 | 12 | 13 | cdef extern from "math.h": 14 | double HUGE_VAL 15 | 16 | 17 | # Python definition 18 | def calc_pcd(double[:, :] X, int n_remove=0): 19 | 20 | cdef: 21 | int N, M, n 22 | cpp_set[int] extremes 23 | vector[int] extremes_min, extremes_max 24 | int[:, :] I 25 | 26 | N = X.shape[0] 27 | M = X.shape[1] 28 | 29 | if n_remove <= (N - M): 30 | if n_remove < 0: 31 | n_remove = 0 32 | else: 33 | pass 34 | else: 35 | n_remove = N - M 36 | 37 | extremes_min = c_get_argmin(X) 38 | extremes_max = c_get_argmax(X) 39 | 40 | extremes = cpp_set[int]() 41 | 42 | for n in extremes_min: 43 | extremes.insert(n) 44 | 45 | for n in extremes_max: 46 | extremes.insert(n) 47 | 48 | _I = np.argsort(X, axis=0, kind='mergesort').astype(np.intc) 49 | I = _I[:, :] 50 | 51 | X = c_normalize_array(X, extremes_max, extremes_min) 52 | 53 | return c_calc_pcd(X, I, n_remove, N, M, extremes) 54 | 55 | 56 | # Returns crowding metrics with recursive elimination 57 | cdef c_calc_pcd(double[:, :] X, int[:, :] I, int n_remove, int N, int M, cpp_set[int] extremes): 58 | 59 | cdef: 60 | int n, n_removed, k 61 | cpp_set[int] calc_items 62 | cpp_set[int] H 63 | double[:, :] D 64 | double[:] d 65 | 66 | # Define items to calculate distances 67 | calc_items = cpp_set[int]() 68 | for n in range(N): 69 | calc_items.insert(n) 70 | for n in extremes: 71 | calc_items.erase(n) 72 | 73 | # Define remaining items to evaluate 74 | H = cpp_set[int]() 75 | for n in range(N): 76 | H.insert(n) 77 | 78 | # Initialize 79 | n_removed = 0 80 | 81 | # Initialize neighbors and distances 82 | _D = np.full((N, M), HUGE_VAL, dtype=np.double) 83 | dd = np.full((N,), HUGE_VAL, dtype=np.double) 84 | 85 | D = _D[:, :] 86 | d = dd[:] 87 | 88 | # Fill in neighbors and distance matrix 89 | c_calc_pcd_iter( 90 | X, 91 | I, 92 | D, 93 | N, M, 94 | calc_items, 95 | ) 96 | 97 | # Obtain distance metrics 98 | c_calc_d(d, D, calc_items, M) 99 | 100 | # While n_remove not acheived 101 | while n_removed < (n_remove - 1): 102 | 103 | # Obtain element to drop 104 | k = c_get_drop(d, H) 105 | H.erase(k) 106 | 107 | # Update index 108 | n_removed = n_removed + 1 109 | 110 | # Get items to be recalculated 111 | calc_items = c_get_calc_items(I, k, M, N) 112 | for n in extremes: 113 | calc_items.erase(n) 114 | 115 | # Fill in neighbors and distance matrix 116 | c_calc_pcd_iter( 117 | X, 118 | I, 119 | D, 120 | N, M, 121 | calc_items, 122 | ) 123 | 124 | # Obtain distance metrics 125 | c_calc_d(d, D, calc_items, M) 126 | 127 | return dd 128 | 129 | 130 | # Iterate 131 | cdef c_calc_pcd_iter( 132 | double[:, :] X, 133 | int[:, :] I, 134 | double[:, :] D, 135 | int N, int M, 136 | cpp_set[int] calc_items, 137 | ): 138 | 139 | cdef: 140 | int i, m, n, l, u 141 | 142 | # Iterate over items to calculate 143 | for i in calc_items: 144 | 145 | # Iterate over elements in X 146 | for m in range(M): 147 | 148 | for n in range(N): 149 | 150 | if i == I[n, m]: 151 | 152 | l = I[n - 1, m] 153 | u = I[n + 1, m] 154 | 155 | D[i, m] = (X[u, m] - X[l, m]) / M 156 | 157 | 158 | # Calculate crowding metric 159 | cdef c_calc_d(double[:] d, double[:, :] D, cpp_set[int] calc_items, int M): 160 | 161 | cdef: 162 | int i, m 163 | 164 | for i in calc_items: 165 | 166 | d[i] = 0 167 | for m in range(M): 168 | d[i] = d[i] + D[i, m] 169 | 170 | 171 | # Returns indexes of items to be recalculated after removal 172 | cdef cpp_set[int] c_get_calc_items( 173 | int[:, :] I, 174 | int k, int M, int N 175 | ): 176 | 177 | cdef: 178 | int n, m 179 | cpp_set[int] calc_items 180 | 181 | calc_items = cpp_set[int]() 182 | 183 | # Iterate over all elements in I 184 | for m in range(M): 185 | 186 | for n in range(N): 187 | 188 | if I[n, m] == k: 189 | 190 | # Add to set of items to be recalculated 191 | calc_items.insert(I[n - 1, m]) 192 | calc_items.insert(I[n + 1, m]) 193 | 194 | # Remove element from sorted array 195 | I[n:-1, m] = I[n + 1:, m] 196 | 197 | return calc_items 198 | -------------------------------------------------------------------------------- /pymoode/cython/spacing_neighbors.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=2, boundscheck=False, wraparound=False, cdivision=True 3 | 4 | import numpy as np 5 | 6 | from libcpp cimport bool 7 | from libcpp.vector cimport vector 8 | from libcpp.set cimport set as cpp_set 9 | 10 | 11 | cdef extern from "math.h": 12 | double HUGE_VAL 13 | 14 | 15 | def calc_spacing_distances(double[:, :] X): 16 | 17 | return c_calc_spacing_distances(X) 18 | 19 | 20 | cdef c_calc_spacing_distances(double[:, :] X): 21 | 22 | cdef: 23 | int N, M, n, m 24 | cpp_set[int] calc_items 25 | double[:, :] D 26 | double[:] d 27 | double dij, _dijm 28 | 29 | N = X.shape[0] 30 | M = X.shape[1] 31 | 32 | # Initialize neighbors and distances 33 | _D = np.full((N, N), -1.0, dtype=np.double) 34 | dd = np.full((N,), HUGE_VAL, dtype=np.double) 35 | 36 | D = _D[:, :] 37 | d = dd[:] 38 | 39 | # Iterate over items to calculate 40 | for i in range(N): 41 | 42 | # Iterate over elements in X 43 | for j in range(N): 44 | 45 | # Calculate distance if elements satisfy rule 46 | if ((j != i) and (D[i, j] <= d[i])): 47 | 48 | # Calculate if different from -1 49 | if D[i, j] == -1.0: 50 | 51 | # Squared distance 52 | dij = 0 53 | for m in range(M): 54 | _dijm = X[j, m] - X[i, m] 55 | 56 | if _dijm >= 0: 57 | dij = dij + _dijm 58 | 59 | else: 60 | dij = dij - _dijm 61 | 62 | # Fill values 63 | D[i, j] = dij 64 | D[j, i] = D[i, j] 65 | 66 | else: 67 | dij = D[i, j] 68 | 69 | # Check is any should be replaced 70 | if (dij <= d[i]): 71 | d[i] = dij 72 | 73 | return dd 74 | -------------------------------------------------------------------------------- /pymoode/cython/utils.pxd: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=2, boundscheck=False, wraparound=False, cdivision=True 3 | 4 | import numpy as np 5 | 6 | from libcpp cimport bool 7 | from libcpp.vector cimport vector 8 | from libcpp.set cimport set as cpp_set 9 | 10 | 11 | cdef extern from "math.h": 12 | double HUGE_VAL 13 | 14 | 15 | # Returns elements to remove based on crowding metric d and heap of remaining elements H 16 | cdef inline int c_get_drop(double[:] d, cpp_set[int] H): 17 | 18 | cdef: 19 | int i, min_i 20 | double min_d 21 | 22 | min_d = HUGE_VAL 23 | min_i = 0 24 | 25 | for i in H: 26 | 27 | if d[i] <= min_d: 28 | min_d = d[i] 29 | min_i = i 30 | 31 | return min_i 32 | 33 | 34 | # Returns vector of positions of minimum values along axis 0 of a 2d memoryview 35 | cdef inline vector[int] c_get_argmin(double[:, :] X): 36 | 37 | cdef: 38 | int N, M, min_i, n, m 39 | double min_val 40 | vector[int] indexes 41 | 42 | N = X.shape[0] 43 | M = X.shape[1] 44 | 45 | indexes = vector[int]() 46 | 47 | for m in range(M): 48 | 49 | min_i = 0 50 | min_val = X[0, m] 51 | 52 | for n in range(N): 53 | 54 | if X[n, m] < min_val: 55 | 56 | min_i = n 57 | min_val = X[n, m] 58 | 59 | indexes.push_back(min_i) 60 | 61 | return indexes 62 | 63 | 64 | # Returns vector of positions of maximum values along axis 0 of a 2d memoryview 65 | cdef inline vector[int] c_get_argmax(double[:, :] X): 66 | 67 | cdef: 68 | int N, M, max_i, n, m 69 | double max_val 70 | vector[int] indexes 71 | 72 | N = X.shape[0] 73 | M = X.shape[1] 74 | 75 | indexes = vector[int]() 76 | 77 | for m in range(M): 78 | 79 | max_i = 0 80 | max_val = X[0, m] 81 | 82 | for n in range(N): 83 | 84 | if X[n, m] > max_val: 85 | 86 | max_i = n 87 | max_val = X[n, m] 88 | 89 | indexes.push_back(max_i) 90 | 91 | return indexes 92 | 93 | 94 | # Performs normalization of a 2d memoryview 95 | cdef inline double[:, :] c_normalize_array(double[:, :] X, vector[int] extremes_max, vector[int] extremes_min): 96 | 97 | cdef: 98 | int N = X.shape[0] 99 | int M = X.shape[1] 100 | int n, m, l, u 101 | double l_val, u_val, diff_val 102 | vector[double] min_vals, max_vals 103 | 104 | min_vals = vector[double]() 105 | max_vals = vector[double]() 106 | 107 | m = 0 108 | for u in extremes_max: 109 | u_val = X[u, m] 110 | max_vals.push_back(u_val) 111 | m = m + 1 112 | 113 | m = 0 114 | for l in extremes_min: 115 | l_val = X[l, m] 116 | min_vals.push_back(l_val) 117 | m = m + 1 118 | 119 | for m in range(M): 120 | 121 | diff_val = max_vals[m] - min_vals[m] 122 | if diff_val == 0.0: 123 | diff_val = 1.0 124 | 125 | for n in range(N): 126 | 127 | X[n, m] = (X[n, m] - min_vals[m]) / diff_val 128 | 129 | return X 130 | -------------------------------------------------------------------------------- /pymoode/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/pymoode/misc/__init__.py -------------------------------------------------------------------------------- /pymoode/misc/mnn.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.spatial.distance import pdist, squareform 3 | 4 | 5 | def calc_mnn(X, n_remove=0, twonn=False): 6 | 7 | N = X.shape[0] 8 | M = X.shape[1] 9 | 10 | if n_remove <= (N - M): 11 | if n_remove < 0: 12 | n_remove = 0 13 | else: 14 | pass 15 | else: 16 | n_remove = N - M 17 | 18 | if twonn: 19 | M = 2 20 | 21 | if N <= M: 22 | return np.full(N, np.inf) 23 | 24 | extremes_min = np.argmin(X, axis=0) 25 | extremes_max = np.argmax(X, axis=0) 26 | 27 | min_vals = np.min(X, axis=0) 28 | max_vals = np.max(X, axis=0) 29 | 30 | extremes = np.concatenate((extremes_min, extremes_max)) 31 | 32 | X = (X - min_vals) / (max_vals - min_vals) 33 | 34 | H = np.arange(N) 35 | 36 | D = squareform(pdist(X, metric="sqeuclidean")) 37 | Dnn = np.partition(D, range(1, M+1), axis=1)[:, 1:M+1] 38 | d = np.prod(Dnn, axis=1) 39 | d[extremes] = np.inf 40 | 41 | n_removed = 0 42 | 43 | #While n_remove not acheived 44 | while n_removed < (n_remove - 1): 45 | 46 | #Obtain element to drop 47 | _d = d[H] 48 | _k = np.argmin(_d) 49 | k = H[_k] 50 | H = H[H != k] 51 | 52 | #Update index 53 | n_removed = n_removed + 1 54 | if n_removed == n_remove: 55 | break 56 | 57 | else: 58 | 59 | D[:, k] = np.inf 60 | Dnn[H] = np.partition(D[H], range(1, M+1), axis=1)[:, 1:M+1] 61 | d[H] = np.prod(Dnn[H], axis=1) 62 | d[extremes] = np.inf 63 | 64 | return d 65 | -------------------------------------------------------------------------------- /pymoode/misc/pruning_cd.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def calc_pcd(X: np.ndarray, n_remove=0): 5 | 6 | N = X.shape[0] 7 | M = X.shape[1] 8 | 9 | if n_remove <= (N - M): 10 | if n_remove < 0: 11 | n_remove = 0 12 | else: 13 | pass 14 | else: 15 | n_remove = N - M 16 | 17 | extremes_min = np.argmin(X, axis=0) 18 | extremes_max = np.argmax(X, axis=0) 19 | 20 | min_vals = np.min(X, axis=0) 21 | max_vals = np.max(X, axis=0) 22 | 23 | extremes = np.concatenate((extremes_min, extremes_max)) 24 | 25 | X = (X - min_vals) / (max_vals - min_vals) 26 | 27 | H = np.arange(N) 28 | d = np.full(N, np.inf) 29 | 30 | I = np.argsort(X, axis=0, kind='mergesort') 31 | 32 | # sort the objective space values for the whole matrix 33 | _X = X[I, np.arange(M)] 34 | 35 | # calculate the distance from each point to the last and next 36 | dist = np.row_stack([_X, np.full(M, np.inf)]) - np.row_stack([np.full(M, -np.inf), _X]) 37 | 38 | # prepare the distance to last and next vectors 39 | dist_to_last, dist_to_next = dist, np.copy(dist) 40 | dist_to_last, dist_to_next = dist_to_last[:-1], dist_to_next[1:] 41 | 42 | # if we divide by zero because all values in one columns are equal replace by none 43 | dist_to_last[np.isnan(dist_to_last)] = 0.0 44 | dist_to_next[np.isnan(dist_to_next)] = 0.0 45 | 46 | # sum up the distance to next and last and norm by objectives - also reorder from sorted list 47 | J = np.argsort(I, axis=0) 48 | _d = np.sum(dist_to_last[J, np.arange(M)] + dist_to_next[J, np.arange(M)], axis=1) 49 | d[H] = _d 50 | d[extremes] = np.inf 51 | 52 | n_removed = 0 53 | 54 | #While n_remove not acheived 55 | while n_removed < (n_remove - 1): 56 | 57 | #Obtain element to drop 58 | _d = d[H] 59 | _k = np.argmin(_d) 60 | k = H[_k] 61 | 62 | H = H[H != k] 63 | 64 | #Update index 65 | n_removed = n_removed + 1 66 | 67 | I = np.argsort(X[H].copy(), axis=0, kind='mergesort') 68 | 69 | # sort the objective space values for the whole matrix 70 | _X = X[H].copy()[I, np.arange(M)] 71 | 72 | # calculate the distance from each point to the last and next 73 | dist = np.row_stack([_X, np.full(M, np.inf)]) - np.row_stack([np.full(M, -np.inf), _X]) 74 | 75 | # prepare the distance to last and next vectors 76 | dist_to_last, dist_to_next = dist, np.copy(dist) 77 | dist_to_last, dist_to_next = dist_to_last[:-1], dist_to_next[1:] 78 | 79 | # if we divide by zero because all values in one columns are equal replace by none 80 | dist_to_last[np.isnan(dist_to_last)] = 0.0 81 | dist_to_next[np.isnan(dist_to_next)] = 0.0 82 | 83 | # sum up the distance to next and last and norm by objectives - also reorder from sorted list 84 | J = np.argsort(I, axis=0) 85 | _d = np.sum(dist_to_last[J, np.arange(M)] + dist_to_next[J, np.arange(M)], axis=1) 86 | d[H] = _d 87 | d[extremes] = np.inf 88 | 89 | # Divide by number of objectives to obtain correct metric 90 | d = d / M 91 | 92 | return d 93 | 94 | -------------------------------------------------------------------------------- /pymoode/operators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/pymoode/operators/__init__.py -------------------------------------------------------------------------------- /pymoode/operators/deop.py: -------------------------------------------------------------------------------- 1 | # Native 2 | from abc import abstractmethod 3 | from typing import Optional, Union 4 | 5 | # External 6 | import numpy as np 7 | 8 | # pymoo 9 | from pymoo.core.crossover import Crossover 10 | from pymoo.core.population import Population 11 | from pymoo.core.problem import Problem 12 | 13 | 14 | # ========================================================================================================= 15 | # Implementation 16 | # ========================================================================================================= 17 | 18 | class DifferentialOperator(Crossover): 19 | 20 | def __init__(self, n_parents=None, **kwargs): 21 | """White label for differential evolution operators 22 | 23 | Parameters 24 | ---------- 25 | n_parents : int | None, optional 26 | Number of parents necessary in its operations. Useful for compatibility with pymoo. 27 | """ 28 | # __init__ operator 29 | super().__init__(n_parents=n_parents, n_offsprings=1, prob=1.0, **kwargs) 30 | 31 | @staticmethod 32 | def default_prepare(pop: Population, parents: Union[Population, np.ndarray]): 33 | """Utility function that converts population and parents from pymoo Selection to pop and X 34 | 35 | Parameters 36 | ---------- 37 | pop : Population 38 | pymoo population 39 | 40 | parents : Population | np.ndarray (n_samples, n_parents) | None 41 | Parent population or indices 42 | 43 | Returns 44 | ------- 45 | pop, X : Population (n_samples, n_parents), np.ndarray (n_parents, n_samples, n_var) 46 | Population and corresponding decision variables 47 | """ 48 | # Convert pop if parents is not None 49 | if parents is not None: 50 | pop = pop[parents] 51 | 52 | # Get all X values for mutation parents 53 | X = np.swapaxes(pop, 0, 1).get("X") 54 | return pop, X 55 | 56 | @abstractmethod 57 | def do( 58 | self, 59 | problem: Problem, 60 | pop: Population, 61 | parents: Optional[Union[Population, np.ndarray]] = None, 62 | **kwargs 63 | ): 64 | pass 65 | 66 | @abstractmethod 67 | def _do(self, problem: Problem, X: np.ndarray, **kwargs): 68 | pass 69 | -------------------------------------------------------------------------------- /pymoode/operators/des.py: -------------------------------------------------------------------------------- 1 | # Native 2 | import warnings 3 | 4 | # External 5 | import numpy as np 6 | 7 | # pymoo imports 8 | from pymoo.core.selection import Selection 9 | from pymoo.core.population import Population 10 | from pymoo.core.problem import Problem 11 | 12 | 13 | # ========================================================================================================= 14 | # Implementation 15 | # ========================================================================================================= 16 | 17 | 18 | # This is the core differential evolution selection class 19 | class DES(Selection): 20 | 21 | def __init__(self, variant: str, **kwargs): 22 | """Differential Evolution parent selection class 23 | 24 | Parameters 25 | ---------- 26 | variant : str, optional 27 | Differential evolution strategy. Must be a string in the format: "DE/selection/n/crossover", in which, n in an integer of number of difference vectors, and crossover is either 'bin' or 'exp'. Selection variants are: 28 | 29 | - 'ranked' 30 | - 'rand' 31 | - 'best' 32 | - 'current-to-best' 33 | - 'current-to-rand' 34 | - 'rand-to-best' 35 | """ 36 | 37 | super().__init__() 38 | self.variant = variant 39 | 40 | def _do( 41 | self, 42 | problem: Problem, 43 | pop: Population, 44 | n_select: int, 45 | n_parents: int, 46 | **kwargs 47 | ): 48 | 49 | # Obtain number of elements in population 50 | n_pop = len(pop) 51 | if n_pop != n_select: 52 | _warn_n_select() 53 | 54 | if self.variant == "ranked": 55 | """Proposed by Zhang et al. (2021). doi.org/10.1016/j.asoc.2021.107317""" 56 | P = self._ranked(pop, n_select, n_parents) 57 | 58 | elif self.variant == "best": 59 | P = self._best(pop, n_select, n_parents) 60 | 61 | elif self.variant == "current-to-best": 62 | P = self._current_to_best(pop, n_select, n_parents) 63 | 64 | elif self.variant == "current-to-rand": 65 | P = self._current_to_rand(pop, n_select, n_parents) 66 | 67 | elif self.variant == "rand-to-best": 68 | P = self._rand_to_best(pop, n_select, n_parents) 69 | 70 | elif self.variant == "rand": 71 | P = self._rand(pop, n_select, n_parents) 72 | 73 | else: 74 | _warn_variant() 75 | P = self._rand(pop, n_select, n_parents) 76 | 77 | return P 78 | 79 | def _rand(self, pop, n_select, n_parents, **kwargs): 80 | 81 | # len of pop 82 | n_pop = len(pop) 83 | 84 | # Base form 85 | P = np.empty([n_select, n_parents], dtype=int) 86 | 87 | # Fill target vector with corresponding parent 88 | target = np.arange(n_pop)[:n_select] 89 | 90 | # Fill next columns in loop 91 | for j in range(n_parents): 92 | 93 | P[:, j] = np.random.choice(n_pop, n_select) 94 | reselect = get_reselect(P, target, j) 95 | 96 | while np.any(reselect): 97 | P[reselect, j] = np.random.choice(n_pop, reselect.sum()) 98 | reselect = get_reselect(P, target, j) 99 | 100 | return P 101 | 102 | def _best(self, pop, n_select, n_parents, **kwargs): 103 | 104 | # len of pop 105 | n_pop = len(pop) 106 | 107 | # Base form 108 | P = np.empty([n_select, n_parents], dtype=int) 109 | 110 | # Fill target vector with corresponding parent 111 | target = np.arange(n_pop)[:n_select] 112 | 113 | # Fill first column with best candidate 114 | P[:, 0] = 0 115 | 116 | # Fill next columns in loop 117 | for j in range(1, n_parents): 118 | 119 | P[:, j] = np.random.choice(n_pop, n_select) 120 | reselect = get_reselect(P, target, j) 121 | 122 | while np.any(reselect): 123 | P[reselect, j] = np.random.choice(n_pop, reselect.sum()) 124 | reselect = get_reselect(P, target, j) 125 | 126 | return P 127 | 128 | def _current_to_best(self, pop, n_select, n_parents, **kwargs): 129 | 130 | # len of pop 131 | n_pop = len(pop) 132 | 133 | # Base form 134 | P = np.empty([n_select, n_parents], dtype=int) 135 | 136 | # Fill target vector with corresponding parent 137 | target = np.arange(n_pop)[:n_select] 138 | 139 | # Fill first column with current candidate 140 | P[:, 0] = np.arange(n_pop) 141 | 142 | # Fill first direction from current 143 | P[:, 2] = np.arange(n_pop) 144 | 145 | # Towards best 146 | P[:, 1] = 0 147 | 148 | # Fill next columns in loop 149 | for j in range(3, n_parents): 150 | 151 | P[:, j] = np.random.choice(n_pop, n_select) 152 | reselect = get_reselect(P, target, j) 153 | 154 | while np.any(reselect): 155 | P[reselect, j] = np.random.choice(n_pop, reselect.sum()) 156 | reselect = get_reselect(P, target, j) 157 | 158 | return P 159 | 160 | def _current_to_rand(self, pop, n_select, n_parents, **kwargs): 161 | 162 | # len of pop 163 | n_pop = len(pop) 164 | 165 | # Base form 166 | P = np.empty([n_select, n_parents], dtype=int) 167 | 168 | # Fill target vector with corresponding parent 169 | target = np.arange(n_pop)[:n_select] 170 | 171 | # Fill first column with current candidate 172 | P[:, 0] = np.arange(n_pop) 173 | 174 | # Fill first direction from current 175 | P[:, 2] = np.arange(n_pop) 176 | 177 | # Towards random 178 | P[:, 1] = np.random.choice(n_pop, n_select) 179 | reselect = get_reselect(P, target, 1) 180 | 181 | while np.any(reselect): 182 | P[reselect, 1] = np.random.choice(n_pop, reselect.sum()) 183 | reselect = get_reselect(P, target, 1) 184 | 185 | # Fill next columns in loop 186 | for j in range(3, n_parents): 187 | 188 | P[:, j] = np.random.choice(n_pop, n_select) 189 | reselect = get_reselect(P, target, j) 190 | 191 | while np.any(reselect): 192 | P[reselect, j] = np.random.choice(n_pop, reselect.sum()) 193 | reselect = get_reselect(P, target, j) 194 | 195 | return P 196 | 197 | def _rand_to_best(self, pop, n_select, n_parents, **kwargs): 198 | 199 | PB = self._best(pop, n_select, n_parents, **kwargs) 200 | P = PB.copy() 201 | 202 | P[:, 0] = PB[:, 1] 203 | P[:, 1] = PB[:, 0] 204 | 205 | return P 206 | 207 | def _ranked(self, pop, n_select, n_parents, **kwargs): 208 | 209 | P = self._rand(pop, n_select, n_parents, **kwargs) 210 | P[:, 1:] = rank_sort(P[:, 1:], pop) 211 | 212 | return P 213 | 214 | 215 | def _warn_n_select(): 216 | warnings.warn( 217 | "DE parent selection is supposed to work with n_select as the population size", 218 | UserWarning 219 | ) 220 | 221 | 222 | def _warn_variant(): 223 | warnings.warn( 224 | "Unknown selection variant; using 'rand' instead", 225 | UserWarning 226 | ) 227 | 228 | 229 | def get_reselect(P: np.ndarray, target: np.ndarray, j: int): 230 | return (P[:, j] == target) | (P[:, j].reshape([-1, 1]) == P[:, :j]).any(axis=1) 231 | 232 | 233 | def ranks_from_cv(pop: Population): 234 | 235 | ranks = pop.get("rank") 236 | cv_elements = ranks == None 237 | 238 | if np.any(cv_elements): 239 | ranks[cv_elements] = np.arange(len(pop))[cv_elements] 240 | 241 | return ranks 242 | 243 | 244 | def rank_sort(P: np.ndarray, pop: Population): 245 | 246 | ranks = ranks_from_cv(pop) 247 | 248 | sorted = np.argsort(ranks[P], axis=1, kind="stable") 249 | S = np.take_along_axis(P, sorted, axis=1) 250 | 251 | P[:, 0] = S[:, 0] 252 | 253 | n_diffs = int((P.shape[1] - 1) / 2) 254 | 255 | for j in range(1, n_diffs + 1): 256 | P[:, 2*j - 1] = S[:, j] 257 | P[:, 2*j] = S[:, -j] 258 | 259 | return P 260 | 261 | 262 | def reiforce_directions(P: np.ndarray, pop: Population): 263 | 264 | ranks = ranks_from_cv(pop) 265 | 266 | ranks = ranks[P] 267 | S = P.copy() 268 | 269 | n_diffs = int(P.shape[1] / 2) 270 | 271 | for j in range(0, n_diffs): 272 | bad_directions = ranks[:, 2*j] > ranks[:, 2*j + 1] 273 | P[bad_directions, 2*j] = S[bad_directions, 2*j + 1] 274 | P[bad_directions, 2*j + 1] = S[bad_directions, 2*j] 275 | 276 | return P 277 | -------------------------------------------------------------------------------- /pymoode/operators/dex.py: -------------------------------------------------------------------------------- 1 | # Native 2 | from abc import abstractmethod 3 | from typing import Optional, Union 4 | 5 | # External 6 | import numpy as np 7 | 8 | # pymoo imports 9 | from pymoo.core.population import Population 10 | from pymoo.core.problem import Problem 11 | 12 | # pymoode imports 13 | from pymoode.operators.deop import DifferentialOperator 14 | 15 | 16 | # ========================================================================================================= 17 | # Implementation 18 | # ========================================================================================================= 19 | 20 | class DifferentialCrossover(DifferentialOperator): 21 | 22 | def __init__(self, 23 | variant="bin", 24 | CR=0.7, 25 | at_least_once=True, 26 | **kwargs): 27 | """Differential evolution crossover 28 | (DE mutation is considered a part of this operator) 29 | 30 | Parameters 31 | ---------- 32 | variant : str | callable, optional 33 | Crossover variant. Must be either "bin", "exp", or callable. By default "bin". 34 | If callable, it has the form: 35 | ``cross_function(n_matings, n_var, CR, at_least_once=True)`` 36 | 37 | CR : float, optional 38 | Crossover parameter. Defined in the range [0, 1] 39 | To reinforce mutation, use higher values. To control convergence speed, use lower values. 40 | 41 | at_least_once : bool, optional 42 | Either or not offsprings must inherit at least one attribute from mutant vectors, by default True 43 | """ 44 | 45 | # __init__ operator 46 | super().__init__(n_parents=2, **kwargs) 47 | 48 | self.CR = CR 49 | self.variant = variant 50 | self.at_least_once = at_least_once 51 | 52 | def do( 53 | self, 54 | problem: Problem, 55 | pop: Population, 56 | parents: Optional[Union[Population, np.ndarray]] = None, 57 | **kwargs 58 | ): 59 | 60 | # Convert pop if parents is not None 61 | pop, X = self.default_prepare(pop, parents) 62 | 63 | # Create child vectors 64 | U = self._do(problem, X, **kwargs) 65 | 66 | return Population.new("X", U) 67 | 68 | @abstractmethod 69 | def _do(self, problem: Problem, X: np.ndarray, **kwargs): 70 | pass 71 | 72 | 73 | class DEX(DifferentialCrossover): 74 | 75 | def __init__(self, 76 | variant="bin", 77 | CR=0.7, 78 | at_least_once=True, 79 | **kwargs): 80 | 81 | super().__init__( 82 | variant=variant, CR=CR, 83 | at_least_once=at_least_once, 84 | **kwargs, 85 | ) 86 | 87 | if self.variant == "bin": 88 | self.cross_function = cross_binomial 89 | elif self.variant == "exp": 90 | self.cross_function = cross_exp 91 | elif hasattr(self.variant, "__call__"): 92 | self.cross_function = self.variant 93 | else: 94 | raise ValueError("Crossover variant must be either 'bin', 'exp', or callable") 95 | 96 | def _do(self, problem: Problem, X: np.ndarray, **kwargs): 97 | 98 | # Decompose input vector 99 | V = X[1] 100 | X_ = X[0] 101 | U = np.array(X_, copy=True) 102 | 103 | # About X 104 | n_matings, n_var = X_.shape 105 | 106 | # Mask 107 | M = self.cross_function(n_matings, n_var, self.CR, self.at_least_once) 108 | U[M] = V[M] 109 | 110 | return U 111 | 112 | 113 | # ========================================================================================================= 114 | # Crossovers 115 | # ========================================================================================================= 116 | 117 | 118 | # From version 0.5.0 of pymoo 119 | def row_at_least_once_true(M: np.ndarray): 120 | 121 | _, d = M.shape 122 | 123 | for k in np.where(~np.any(M, axis=1))[0]: 124 | M[k, np.random.randint(d)] = True 125 | 126 | return M 127 | 128 | 129 | def cross_binomial(n_matings, n_var, prob, at_least_once=True): 130 | 131 | M = np.random.random((n_matings, n_var)) < prob 132 | 133 | if at_least_once: 134 | M = row_at_least_once_true(M) 135 | 136 | return M 137 | 138 | 139 | def cross_exp(n_matings, n_var, prob, at_least_once=True): 140 | 141 | # the mask do to the crossover 142 | M = np.full((n_matings, n_var), False) 143 | 144 | # start point of crossover 145 | s = np.random.randint(0, n_var, size=n_matings) 146 | 147 | # create for each individual the crossover range 148 | for i in range(n_matings): 149 | 150 | # the actual index where we start 151 | start = s[i] 152 | for j in range(n_var): 153 | 154 | # the current position where we are pointing to 155 | current = (start + j) % n_var 156 | 157 | # replace only if random value keeps being smaller than CR 158 | if np.random.random() <= prob: 159 | M[i, current] = True 160 | else: 161 | break 162 | 163 | if at_least_once: 164 | M = row_at_least_once_true(M) 165 | 166 | return M 167 | -------------------------------------------------------------------------------- /pymoode/operators/variant.py: -------------------------------------------------------------------------------- 1 | # Native 2 | import warnings 3 | 4 | # External 5 | import numpy as np 6 | 7 | # pymoo imports 8 | from pymoo.operators.mutation.nom import NoMutation 9 | from pymoo.core.infill import InfillCriterion 10 | from pymoo.core.population import Population 11 | from pymoo.core.problem import Problem 12 | 13 | # pymoode imports 14 | from pymoode.operators.des import DES 15 | from pymoode.operators.dex import DEX 16 | from pymoode.operators.dem import DEM 17 | 18 | 19 | # ========================================================================================================= 20 | # Implementation 21 | # ========================================================================================================= 22 | 23 | class DifferentialVariant(InfillCriterion): 24 | 25 | def __init__(self, 26 | variant="DE/rand/1/bin", 27 | CR=0.7, 28 | F=(0.5, 1.0), 29 | gamma=1e-4, 30 | de_repair="bounce-back", 31 | genetic_mutation=None, 32 | **kwargs): 33 | """InfillCriterion class for Differential Evolution 34 | 35 | Parameters 36 | ---------- 37 | variant : str, optional 38 | Differential evolution strategy. Must be a string in the format: "DE/selection/n/crossover", in which, n in an integer of number of difference vectors, and crossover is either 'bin' or 'exp'. Selection variants are: 39 | 40 | - 'ranked' 41 | - 'rand' 42 | - 'best' 43 | - 'current-to-best' 44 | - 'current-to-rand' 45 | - 'rand-to-best' 46 | 47 | The selection strategy 'ranked' might be helpful to improve convergence speed without much harm to diversity. Defaults to 'DE/rand/1/bin'. 48 | 49 | CR : float, optional 50 | Crossover parameter. Defined in the range [0, 1] 51 | To reinforce mutation, use higher values. To control convergence speed, use lower values. 52 | 53 | F : iterable of float or float, optional 54 | Scale factor or mutation parameter. Defined in the range (0, 2] 55 | To reinforce exploration, use higher values; for exploitation, use lower values. 56 | 57 | gamma : float, optional 58 | Jitter deviation parameter. Should be in the range (0, 2). Defaults to 1e-4. 59 | 60 | de_repair : str, optional 61 | Repair of DE mutant vectors. Is either callable or one of: 62 | 63 | - 'bounce-back' 64 | - 'midway' 65 | - 'rand-init' 66 | - 'to-bounds' 67 | 68 | If callable, has the form fun(X, Xb, xl, xu) in which X contains mutated vectors including violations, Xb contains reference vectors for repair in feasible space, xl is a 1d vector of lower bounds, and xu a 1d vector of upper bounds. 69 | Defaults to 'bounce-back'. 70 | 71 | genetic_mutation : Mutation, optional 72 | Pymoo's genetic algorithm's mutation operator after crossover. Defaults to NoMutation(). 73 | 74 | repair : Repair, optional 75 | Pymoo's repair operator after mutation. Defaults to NoRepair(). 76 | """ 77 | 78 | # Fix deprecated pm kwargs 79 | kwargs, genetic_mutation = _fix_deprecated_pm_kwargs(kwargs, genetic_mutation) 80 | 81 | # Default initialization of InfillCriterion 82 | super().__init__(eliminate_duplicates=None, **kwargs) 83 | 84 | # Parse the information from the string 85 | _, selection_variant, n_diff, crossover_variant, = variant.split("/") 86 | n_diffs = int(n_diff) 87 | 88 | # When "to" in variant there are more than 1 difference vectors 89 | if "-to-" in variant: 90 | n_diffs = n_diffs + 1 91 | 92 | # Define parent selection operator 93 | self.selection = DES(selection_variant) 94 | 95 | # Define differential evolution mutation 96 | self.de_mutation = DEM(F=F, gamma=gamma, de_repair=de_repair, n_diffs=n_diffs) 97 | 98 | # Define crossover strategy (DE mutation is included) 99 | self.crossover = DEX(variant=crossover_variant, CR=CR, at_least_once=True) 100 | 101 | # Define posterior mutation strategy and repair 102 | self.genetic_mutation = genetic_mutation if genetic_mutation is not None else NoMutation() 103 | 104 | def _do( 105 | self, 106 | problem: Problem, 107 | pop: Population, 108 | n_offsprings: int, 109 | **kwargs 110 | ): 111 | 112 | # Select parents including donor vector 113 | parents = self.selection( 114 | problem, pop, 115 | n_offsprings, self.n_parents, 116 | to_pop=False, **kwargs, 117 | ) 118 | 119 | # Mutant vectors from DE 120 | mutants = self.de_mutation(problem, pop, parents, **kwargs) 121 | 122 | # Crossover between mutant vector and target 123 | matings = self.merge_columnwise(pop, mutants) 124 | trials = self.crossover(problem, matings, **kwargs) 125 | 126 | # Perform posterior mutation and repair if passed 127 | off = self.genetic_mutation(problem, trials, **kwargs) 128 | 129 | return off 130 | 131 | @property 132 | def n_parents(self): 133 | return self.de_mutation.n_parents 134 | 135 | @staticmethod 136 | def merge_columnwise(*populations): 137 | matings = np.column_stack(populations).view(Population) 138 | return matings 139 | 140 | 141 | 142 | def _fix_deprecated_pm_kwargs(kwargs, genetic_mutation): 143 | if "pm" in kwargs: 144 | warnings.warn( 145 | "pm is deprecated; use 'genetic_mutation'", 146 | DeprecationWarning, 2 147 | ) 148 | if genetic_mutation is None: 149 | genetic_mutation = kwargs["pm"] 150 | elif "mutation" in kwargs: 151 | warnings.warn( 152 | "mutation is deprecated; use 'genetic_mutation'", 153 | DeprecationWarning, 2 154 | ) 155 | if genetic_mutation is None: 156 | genetic_mutation = kwargs["mutation"] 157 | 158 | return kwargs, genetic_mutation 159 | -------------------------------------------------------------------------------- /pymoode/performance/__init__.py: -------------------------------------------------------------------------------- 1 | from pymoode.performance._spacing import SpacingIndicator -------------------------------------------------------------------------------- /pymoode/performance/_spacing.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.spatial.distance import pdist, squareform 3 | from pymoo.core.indicator import Indicator 4 | from pymoo.indicators.distance_indicator import at_least_2d_array, derive_ideal_and_nadir_from_pf 5 | 6 | 7 | # ========================================================================================================= 8 | # Implementation 9 | # ========================================================================================================= 10 | 11 | class SpacingIndicator(Indicator): 12 | 13 | def __init__(self, 14 | metric="cityblock", 15 | pf=None, 16 | zero_to_one=False, 17 | ideal=None, 18 | nadir=None): 19 | """Spacing indicator 20 | The smaller the value this indicator assumes, the most uniform is the distribution of elements on the pareto front. 21 | 22 | Parameters 23 | ---------- 24 | metric : str, optional 25 | Distance metric parsed to scipy.spatial.distance.pdist, by default "cityblock" 26 | 27 | pf : 2d array, optional 28 | Pareto front, by default None 29 | 30 | zero_to_one : bool, optional 31 | Whether or not the objective values should be normalized in calculations, by default False 32 | 33 | ideal : 1d array, optional 34 | Ideal point, by default None 35 | 36 | nadir : 1d array, optional 37 | Nadir point, by default None 38 | """ 39 | 40 | # the pareto front if necessary to calculate the indicator 41 | pf = at_least_2d_array(pf, extend_as="row") 42 | ideal, nadir = derive_ideal_and_nadir_from_pf(pf, ideal=ideal, nadir=nadir) 43 | 44 | super().__init__(pf=pf, 45 | zero_to_one=zero_to_one, 46 | ideal=ideal, 47 | nadir=nadir) 48 | 49 | self.metric = metric 50 | 51 | def do(self, F, *args, **kwargs): 52 | """Obtain the spacing indicator given a Pareto front 53 | 54 | Parameters 55 | ---------- 56 | F : numpy.array (n_samples, n_obj) 57 | Pareto front 58 | 59 | Returns 60 | ------- 61 | float 62 | Spacing indicator 63 | """ 64 | return super().do(F, *args, **kwargs) 65 | 66 | def _do(self, F, *args, **kwargs): 67 | 68 | # Get F dimensions 69 | n_points, n_obj = F.shape 70 | 71 | # knn 72 | D = squareform(pdist(F, metric=self.metric)) 73 | d = np.partition(D, 1, axis=1)[:, 1] 74 | dm = np.mean(d) 75 | 76 | # Get spacing 77 | S = np.sqrt(np.sum(np.square(d - dm)) / n_points) 78 | 79 | return S 80 | -------------------------------------------------------------------------------- /pymoode/survival/__init__.py: -------------------------------------------------------------------------------- 1 | from pymoode.survival.rank_and_crowding.rnc import RankAndCrowding, ConstrRankAndCrowding -------------------------------------------------------------------------------- /pymoode/survival/fitness.py: -------------------------------------------------------------------------------- 1 | # Native 2 | from typing import Optional 3 | 4 | # External 5 | import numpy as np 6 | 7 | # pymoo imports 8 | from pymoo.core.survival import Survival 9 | from pymoo.core.population import Population 10 | from pymoo.core.problem import Problem 11 | 12 | 13 | # ========================================================================================================= 14 | # Implementation 15 | # ========================================================================================================= 16 | 17 | class BaseFitnessSurvival(Survival): 18 | 19 | def __init__(self) -> None: 20 | super().__init__(filter_infeasible=False) 21 | 22 | def _do( 23 | self, 24 | problem: Problem, 25 | pop: Population, 26 | n_survive: Optional[int]=None, 27 | **kwargs 28 | ): 29 | return pop[:n_survive] 30 | 31 | 32 | class FitnessSurvival(Survival): 33 | 34 | def __init__(self) -> None: 35 | super().__init__(filter_infeasible=False) 36 | 37 | def _do( 38 | self, 39 | problem: Problem, 40 | pop: Population, 41 | n_survive: Optional[int]=None, 42 | **kwargs 43 | ): 44 | F, cv = pop.get("F", "cv") 45 | assert F.shape[1] == 1, "FitnessSurvival can only used for single objective single!" 46 | S = np.lexsort([F[:, 0], cv]) 47 | pop.set("rank", np.argsort(S)) 48 | return pop[S[:n_survive]] 49 | -------------------------------------------------------------------------------- /pymoode/survival/rank_and_crowding/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/pymoode/survival/rank_and_crowding/__init__.py -------------------------------------------------------------------------------- /pymoode/survival/rank_and_crowding/metrics.py: -------------------------------------------------------------------------------- 1 | # External 2 | import numpy as np 3 | from scipy.spatial.distance import pdist, squareform 4 | 5 | # pymoo imports 6 | from pymoo.util.misc import find_duplicates 7 | 8 | 9 | # ========================================================================================================= 10 | # Checks compilation 11 | # ========================================================================================================= 12 | 13 | try: 14 | from pymoode.cython.info import info 15 | if info() == "This file was based on pymoo to check compilation success": 16 | IS_COMPILED = True 17 | else: 18 | IS_COMPILED = False 19 | except Exception: 20 | IS_COMPILED = False 21 | 22 | 23 | # ========================================================================================================= 24 | # pymoode imports and definitions 25 | # ========================================================================================================= 26 | 27 | 28 | if IS_COMPILED: 29 | 30 | from pymoode.cython.mnn import calc_mnn, calc_2nn 31 | from pymoode.cython.pruning_cd import calc_pcd 32 | 33 | def calc_mnn_nds(F, n_remove: int = 0): 34 | return calc_mnn(F, n_remove=n_remove) 35 | 36 | def calc_2nn_nds(F, n_remove: int = 0): 37 | return calc_2nn(F, n_remove=n_remove) 38 | 39 | def calc_pcd_nds(F, n_remove: int = 0): 40 | return calc_pcd(F, n_remove=n_remove) 41 | 42 | else: 43 | 44 | print("WARNING: Failed to import cython modules. Slow implementation on.") 45 | from pymoode.misc.mnn import calc_mnn 46 | from pymoode.misc.pruning_cd import calc_pcd 47 | 48 | def calc_mnn_nds(F, n_remove: int = 0): 49 | return calc_mnn(F, n_remove=n_remove, twonn=False) 50 | 51 | def calc_2nn_nds(F, n_remove: int = 0): 52 | return calc_mnn(F, n_remove=n_remove, twonn=True) 53 | 54 | def calc_pcd_nds(F, n_remove: int = 0): 55 | return calc_pcd(F, n_remove=n_remove) 56 | 57 | 58 | def get_crowding_function(label): 59 | 60 | if label == "cd": 61 | fun = FunctionalDiversity(calc_crowding_distance, filter_out_duplicates=False) 62 | elif (label == "pcd") or (label == "pruning-cd"): 63 | fun = FunctionalDiversity(calc_pcd_nds, filter_out_duplicates=True) 64 | elif label == "ce": 65 | fun = FunctionalDiversity(calc_crowding_entropy, filter_out_duplicates=True) 66 | elif label == "mnn": 67 | fun = FuncionalDiversityMNN(calc_mnn_nds, filter_out_duplicates=True) 68 | elif label == "2nn": 69 | fun = FuncionalDiversityMNN(calc_2nn_nds, filter_out_duplicates=True) 70 | elif hasattr(label, "__call__"): 71 | fun = FunctionalDiversity(label, filter_out_duplicates=True) 72 | elif isinstance(label, CrowdingDiversity): 73 | fun = label 74 | else: 75 | raise KeyError("Crwoding function not defined") 76 | return fun 77 | 78 | 79 | # ========================================================================================================= 80 | # Implementation 81 | # ========================================================================================================= 82 | 83 | 84 | class CrowdingDiversity: 85 | 86 | def __init__(self, *args, **kwargs) -> None: 87 | """Instantiates an operator that calculates crowding metrics given an array of objectives F 88 | """ 89 | pass 90 | 91 | def do(self, F: np.ndarray, n_remove: int = 0, **kwargs) -> np.ndarray: 92 | """Computes diversity metric for a given objective space 93 | 94 | Parameters 95 | ---------- 96 | F : 2d array like 97 | Objective space (each row represents an individual and each column an objective) 98 | 99 | n_remove : int, optional 100 | Number of individuals to be removed (in case of recursive elimination), by default 0 101 | 102 | Returns 103 | ------- 104 | np.ndarray 105 | Diversity metric (1d) 106 | """ 107 | # Converting types Python int to Cython int would fail in some cases converting to long instead 108 | n_remove = np.intc(n_remove) 109 | F = np.array(F, dtype=np.double) 110 | return self._do(F, n_remove=n_remove, **kwargs) 111 | 112 | def _do(self, F, n_remove=None, **kwargs): 113 | pass 114 | 115 | 116 | class FunctionalDiversity(CrowdingDiversity): 117 | 118 | def __init__(self, function=None, filter_out_duplicates=True): 119 | """Operator to evaluate crowding metrics with useful strategies of filtering out duplicates 120 | 121 | Parameters 122 | ---------- 123 | function : Callable, optional 124 | Function in the format ``func(F, n_remove=None, **kwargs)`` 125 | that should return crowding metrics. By default None 126 | 127 | filter_out_duplicates : bool, optional 128 | Either of not to filter out duplicates before calculating crowding metrics, by default True 129 | """ 130 | self.function = function 131 | self.filter_out_duplicates = filter_out_duplicates 132 | super().__init__() 133 | 134 | def _do(self, F, **kwargs): 135 | 136 | n_points, _ = F.shape 137 | 138 | if n_points <= 2: 139 | return np.full(n_points, np.inf) 140 | 141 | else: 142 | 143 | if self.filter_out_duplicates: 144 | # filter out solutions which are duplicates - duplicates get a zero finally 145 | is_unique = np.where(np.logical_not(find_duplicates(F, epsilon=1e-32)))[0] 146 | else: 147 | # set every point to be unique without checking it 148 | is_unique = np.arange(n_points) 149 | 150 | # index the unique points of the array 151 | _F = F[is_unique] 152 | 153 | _d = self.function(_F, **kwargs) 154 | 155 | d = np.zeros(n_points) 156 | d[is_unique] = _d 157 | 158 | return d 159 | 160 | 161 | class FuncionalDiversityMNN(FunctionalDiversity): 162 | 163 | def _do(self, F, **kwargs): 164 | 165 | n_points, n_obj = F.shape 166 | 167 | if n_points <= n_obj: 168 | return np.full(n_points, np.inf) 169 | 170 | else: 171 | return super()._do(F, **kwargs) 172 | 173 | 174 | def calc_crowding_entropy(F, **kwargs) -> np.ndarray: 175 | """Wang, Y.-N., Wu, L.-H. & Yuan, X.-F., 2010. Multi-objective self-adaptive differential 176 | evolution with elitist archive and crowding entropy-based diversity measure. 177 | Soft Comput., 14(3), pp. 193-209. 178 | 179 | Parameters 180 | ---------- 181 | F : 2d array like 182 | Objective space (each row represents an individual and each column an objective) 183 | 184 | Returns 185 | ------- 186 | crowding_enropies : 1d array 187 | """ 188 | 189 | n_points, n_obj = F.shape 190 | 191 | # sort each column and get index 192 | I = np.argsort(F, axis=0, kind='mergesort') 193 | 194 | # sort the objective space values for the whole matrix 195 | F = F[I, np.arange(n_obj)] 196 | 197 | # calculate the distance from each point to the last and next 198 | dist = np.row_stack([F, np.full(n_obj, np.inf)]) - np.row_stack([np.full(n_obj, -np.inf), F]) 199 | 200 | # calculate the norm for each objective - set to NaN if all values are equal 201 | norm = np.max(F, axis=0) - np.min(F, axis=0) 202 | norm[norm == 0] = np.nan 203 | 204 | # prepare the distance to last and next vectors 205 | dl = dist.copy()[:-1] 206 | du = dist.copy()[1:] 207 | 208 | # Fix nan 209 | dl[np.isnan(dl)] = 0.0 210 | du[np.isnan(du)] = 0.0 211 | 212 | # Total distance 213 | cd = dl + du 214 | 215 | # Get relative positions 216 | pl = (dl[1:-1] / cd[1:-1]) 217 | pu = (du[1:-1] / cd[1:-1]) 218 | 219 | # Entropy 220 | entropy = np.row_stack([np.full(n_obj, np.inf), 221 | -(pl * np.log2(pl) + pu * np.log2(pu)), 222 | np.full(n_obj, np.inf)]) 223 | 224 | # Crowding entropy 225 | J = np.argsort(I, axis=0) 226 | _cej = cd[J, np.arange(n_obj)] * entropy[J, np.arange(n_obj)] / norm 227 | _cej[np.isnan(_cej)] = 0.0 228 | ce = _cej.sum(axis=1) 229 | 230 | return ce 231 | 232 | 233 | def calc_crowding_distance(F: np.ndarray, **kwargs) -> np.ndarray: 234 | """Pymoo's native function to compute crowding distances 235 | 236 | Parameters 237 | ---------- 238 | F : 2d array like 239 | Objective space (each row represents an individual and each column an objective) 240 | 241 | Returns 242 | ------- 243 | crwoding_distances : 1d array 244 | """ 245 | _, n_obj = F.shape 246 | 247 | # sort each column and get index 248 | I = np.argsort(F, axis=0, kind='mergesort') 249 | 250 | # sort the objective space values for the whole matrix 251 | F = F[I, np.arange(n_obj)] 252 | 253 | # calculate the distance from each point to the last and next 254 | dist = np.row_stack([F, np.full(n_obj, np.inf)]) - np.row_stack([np.full(n_obj, -np.inf), F]) 255 | 256 | # calculate the norm for each objective - set to NaN if all values are equal 257 | norm = np.max(F, axis=0) - np.min(F, axis=0) 258 | norm[norm == 0] = np.nan 259 | 260 | # prepare the distance to last and next vectors 261 | dist_to_last, dist_to_next = dist, np.copy(dist) 262 | dist_to_last, dist_to_next = dist_to_last[:-1] / norm, dist_to_next[1:] / norm 263 | 264 | # if we divide by zero because all values in one columns are equal replace by none 265 | dist_to_last[np.isnan(dist_to_last)] = 0.0 266 | dist_to_next[np.isnan(dist_to_next)] = 0.0 267 | 268 | # sum up the distance to next and last and norm by objectives - also reorder from sorted list 269 | J = np.argsort(I, axis=0) 270 | cd = np.sum(dist_to_last[J, np.arange(n_obj)] + dist_to_next[J, np.arange(n_obj)], axis=1) / n_obj 271 | 272 | return cd 273 | 274 | 275 | def calc_mnn_fast(F, **kwargs): 276 | return _calc_mnn_fast(F, F.shape[1], **kwargs) 277 | 278 | 279 | def calc_2nn_fast(F, **kwargs): 280 | return _calc_mnn_fast(F, 2, **kwargs) 281 | 282 | 283 | def _calc_mnn_fast(F, n_neighbors, **kwargs): 284 | 285 | # calculate the norm for each objective - set to NaN if all values are equal 286 | norm = np.max(F, axis=0) - np.min(F, axis=0) 287 | norm[norm == 0] = 1.0 288 | 289 | # F normalized 290 | F = (F - F.min(axis=0)) / norm 291 | 292 | # Distances pairwise (Inefficient) 293 | D = squareform(pdist(F, metric="sqeuclidean")) 294 | 295 | # M neighbors 296 | M = F.shape[1] 297 | _D = np.partition(D, range(1, M+1), axis=1)[:, 1:M+1] 298 | 299 | # Metric d 300 | d = np.prod(_D, axis=1) 301 | 302 | # Set top performers as np.inf 303 | _extremes = np.concatenate((np.argmin(F, axis=0), np.argmax(F, axis=0))) 304 | d[_extremes] = np.inf 305 | 306 | return d 307 | -------------------------------------------------------------------------------- /pymoode/survival/rank_and_crowding/rnc.py: -------------------------------------------------------------------------------- 1 | # Native 2 | import warnings 3 | from typing import Optional 4 | 5 | # External 6 | import numpy as np 7 | 8 | # pymoo imports 9 | from pymoo.util.randomized_argsort import randomized_argsort 10 | from pymoo.util.nds.non_dominated_sorting import NonDominatedSorting 11 | from pymoo.core.survival import Survival, split_by_feasibility 12 | from pymoo.core.population import Population 13 | from pymoo.core.problem import Problem 14 | 15 | # pymoode imports 16 | from pymoode.survival.rank_and_crowding.metrics import get_crowding_function 17 | 18 | 19 | # ========================================================================================================= 20 | # Implementation 21 | # ========================================================================================================= 22 | 23 | class RankAndCrowding(Survival): 24 | 25 | def __init__( 26 | self, 27 | nds=None, 28 | crowding_func="cd", 29 | rule=None 30 | ): 31 | """ 32 | A generalization of the NSGA-II survival operator that ranks individuals by dominance criteria 33 | and sorts the last front by some crowding metric. 34 | 35 | For many-objective problems, try using 'mnn' or '2nn'. 36 | 37 | For Bi-objective problems, 'pcd' is very effective. 38 | 39 | Parameters 40 | ---------- 41 | nds : str or None, optional 42 | Pymoo type of non-dominated sorting. Defaults to None. 43 | 44 | crowding_func : str or callable, optional 45 | Crowding metric. Options are: 46 | 47 | - 'cd': crowding distances 48 | - 'pcd' or 'pruning-cd': improved pruning based on crowding distances 49 | - 'ce': crowding entropy 50 | - 'mnn': M-Neaest Neighbors 51 | - '2nn': 2-Neaest Neighbors 52 | 53 | If callable, it has the form ``fun(F, n_remove=None, **kwargs)`` 54 | in which F (n, m) and must return metrics in a (n,) array. 55 | 56 | The options 'pcd', 'cd', and 'ce' are recommended for two-objective problems, whereas 'mnn' and '2nn' for many objective. 57 | When using 'pcd', 'mnn', or '2nn', individuals are already eliminated in a 'single' manner. 58 | Due to Cython implementation, they are as fast as the corresponding 'cd', 'mnn-fast', or '2nn-fast', 59 | although they can singnificantly improve diversity of solutions. 60 | Defaults to 'cd'. 61 | """ 62 | if rule is not None: 63 | warnings.warn( 64 | "rule is deprecated; elimination methods are now included in crowding_func", 65 | DeprecationWarning, 2 66 | ) 67 | 68 | crowding_func_ = get_crowding_function(crowding_func) 69 | 70 | super().__init__(filter_infeasible=True) 71 | self.nds = nds if nds is not None else NonDominatedSorting() 72 | self.crowding_func = crowding_func_ 73 | 74 | def _do( 75 | self, 76 | problem: Problem, 77 | pop: Population, 78 | *args, 79 | n_survive: Optional[int] = None, 80 | **kwargs 81 | ): 82 | 83 | # get the objective space values and objects 84 | F = pop.get("F").astype(float, copy=False) 85 | 86 | # the final indices of surviving individuals 87 | survivors = [] 88 | 89 | # do the non-dominated sorting until splitting front 90 | fronts = self.nds.do(F, n_stop_if_ranked=n_survive) 91 | 92 | for k, front in enumerate(fronts): 93 | 94 | I = np.arange(len(front)) 95 | 96 | # current front sorted by crowding distance if splitting 97 | if len(survivors) + len(I) > n_survive: 98 | 99 | # Define how many will be removed 100 | n_remove = len(survivors) + len(front) - n_survive 101 | 102 | # re-calculate the crowding distance of the front 103 | crowding_of_front = \ 104 | self.crowding_func.do( 105 | F[front, :], 106 | n_remove=n_remove 107 | ) 108 | 109 | I = randomized_argsort(crowding_of_front, order='descending', method='numpy') 110 | I = I[:-n_remove] 111 | 112 | # otherwise take the whole front unsorted 113 | else: 114 | # calculate the crowding distance of the front 115 | crowding_of_front = \ 116 | self.crowding_func.do( 117 | F[front, :], 118 | n_remove=0 119 | ) 120 | 121 | # save rank and crowding in the individual class 122 | for j, i in enumerate(front): 123 | pop[i].set("rank", k) 124 | pop[i].set("crowding", crowding_of_front[j]) 125 | 126 | # extend the survivors by all or selected individuals 127 | survivors.extend(front[I]) 128 | 129 | return pop[survivors] 130 | 131 | 132 | class ConstrRankAndCrowding(Survival): 133 | 134 | def __init__(self, nds=None, crowding_func="cd", ranking=None): 135 | """ 136 | The Rank and Crowding survival approach for handling constraints proposed on 137 | GDE3 by Kukkonen, S. & Lampinen, J. (2005). 138 | 139 | Parameters 140 | ---------- 141 | nds : str or None, optional 142 | Pymoo type of non-dominated sorting. Defaults to None. 143 | 144 | crowding_func : str or callable, optional 145 | Crowding metric. Options are: 146 | 147 | - 'cd': crowding distances 148 | - 'pcd' or 'pruning-cd': improved pruning based on crowding distances 149 | - 'ce': crowding entropy 150 | - 'mnn': M-Neaest Neighbors 151 | - '2nn': 2-Neaest Neighbors 152 | 153 | If callable, it has the form ``fun(F, n_remove=None, **kwargs)`` 154 | in which F (n, m) and must return metrics in a (n,) array. 155 | 156 | The options 'pcd', 'cd', and 'ce' are recommended for two-objective problems, whereas 'mnn' and '2nn' for many objective. 157 | When using 'pcd', 'mnn', or '2nn', individuals are already eliminated in a 'single' manner. 158 | Due to Cython implementation, they are as fast as the corresponding 'cd', 'mnn-fast', or '2nn-fast', 159 | although they can singnificantly improve diversity of solutions. 160 | Defaults to 'cd'. 161 | """ 162 | if ranking is not None: 163 | warnings.warn( 164 | "'ranking' is deprecated; internal survival operators are now constructed from nds and crowding_func", 165 | DeprecationWarning, 2 166 | ) 167 | 168 | super().__init__(filter_infeasible=False) 169 | self.nds = nds if nds is not None else NonDominatedSorting() 170 | self.ranking = RankAndCrowding(nds=nds, crowding_func=crowding_func) 171 | 172 | def _do( 173 | self, 174 | problem: Problem, 175 | pop: Population, 176 | *args, 177 | n_survive: Optional[int] = None, 178 | **kwargs 179 | ): 180 | 181 | if n_survive is None: 182 | n_survive = len(pop) 183 | 184 | n_survive = min(n_survive, len(pop)) 185 | 186 | # If the split should be done beforehand 187 | if problem.n_constr > 0: 188 | 189 | # Split by feasibility 190 | feas, infeas = split_by_feasibility(pop, sort_infeas_by_cv=True, sort_feas_by_obj=False, return_pop=False) 191 | 192 | # Obtain len of feasible 193 | n_feas = len(feas) 194 | 195 | # Assure there is at least_one survivor 196 | if n_feas == 0: 197 | survivors = Population() 198 | else: 199 | survivors = self.ranking.do(problem, pop[feas], *args, n_survive=min(len(feas), n_survive), **kwargs) 200 | 201 | # Calculate how many individuals are still remaining to be filled up with infeasible ones 202 | n_remaining = n_survive - len(survivors) 203 | 204 | # If infeasible solutions need to be added 205 | if n_remaining > 0: 206 | 207 | # Constraints to new ranking 208 | G = pop[infeas].get("G") 209 | G = np.maximum(G, 0) 210 | H = pop[infeas].get("H") 211 | H = np.absolute(H) 212 | C = np.column_stack((G, H)) 213 | 214 | # Fronts in infeasible population 215 | infeas_fronts = self.nds.do(C, n_stop_if_ranked=n_remaining) 216 | 217 | # Iterate over fronts 218 | for k, front in enumerate(infeas_fronts): 219 | 220 | # Save ranks 221 | pop[infeas][front].set("cv_rank", k) 222 | 223 | # Current front sorted by CV 224 | if len(survivors) + len(front) > n_survive: 225 | 226 | # Obtain CV of front 227 | CV = pop[infeas][front].get("CV").flatten() 228 | I = randomized_argsort(CV, order='ascending', method='numpy') 229 | I = I[:(n_survive - len(survivors))] 230 | 231 | # Otherwise take the whole front unsorted 232 | else: 233 | I = np.arange(len(front)) 234 | 235 | # extend the survivors by all or selected individuals 236 | survivors = Population.merge(survivors, pop[infeas][front[I]]) 237 | 238 | else: 239 | survivors = self.ranking.do(problem, pop, *args, n_survive=n_survive, **kwargs) 240 | 241 | return survivors 242 | -------------------------------------------------------------------------------- /pymoode/survival/replacement.py: -------------------------------------------------------------------------------- 1 | # Native 2 | from typing import Optional 3 | 4 | # External 5 | import numpy as np 6 | 7 | # pymoo imports 8 | from pymoo.core.duplicate import DefaultDuplicateElimination 9 | from pymoo.core.individual import Individual 10 | from pymoo.core.population import Population 11 | from pymoo.core.problem import Problem 12 | from pymoo.core.survival import Survival 13 | from pymoo.core.individual import Individual 14 | 15 | # pymoode imports 16 | from pymoode.survival.fitness import FitnessSurvival 17 | 18 | 19 | # ========================================================================================================= 20 | # Implementation 21 | # ========================================================================================================= 22 | 23 | def is_better(_new: Individual, _old: Individual, eps=0.0): 24 | both_infeasible = not _old.feasible[0] and not _new.feasible[0] 25 | both_feasible = _old.feasible[0] and _new.feasible[0] 26 | 27 | if both_infeasible and _old.CV[0] - _new.CV[0] > eps: 28 | return True 29 | elif not _old.feasible and _new.feasible: 30 | return True 31 | elif both_feasible and _old.F[0] - _new.F[0] > eps: 32 | return True 33 | 34 | return False 35 | 36 | 37 | class BaseReplacement(Survival): 38 | 39 | def __init__( 40 | self, 41 | fitness: Optional[Survival] = None 42 | ): 43 | """Base class for Replacement survival 44 | 45 | Parameters 46 | ---------- 47 | fitness : Survival, optional 48 | Survival used in ranking and fitness assignement, by default None 49 | """ 50 | super().__init__(filter_infeasible=False) 51 | if fitness is None: 52 | fitness = FitnessSurvival() 53 | self.fitness = fitness 54 | 55 | def do( 56 | self, 57 | problem: Problem, 58 | pop: Population, 59 | off: Population, 60 | return_indices=False, 61 | inplace=False, 62 | **kwargs 63 | ): 64 | """Given a problem a parent population and offspring canditates return next generation. 65 | By default, only fitness assigment is performed on parents. To define which ones should be replaced, 66 | define method ``_do`` in child classes that returns a boolean if parent should be replaced. 67 | 68 | Parameters 69 | ---------- 70 | problem : Problem 71 | Pymoo problem 72 | 73 | pop : Population 74 | Parent population 75 | 76 | off : Population | None 77 | Offspring candidates. If None, only fitness assigment is performed on parents. 78 | 79 | return_indices : bool, optional 80 | Either or not to just return boolean of positions to be replaced, by default False 81 | 82 | inplace : bool, optional 83 | Change population inplace, by default False 84 | 85 | Returns 86 | ------- 87 | Population 88 | Population that proceeds into the next generation 89 | """ 90 | 91 | # If no offspring is available just do fitness assignment 92 | if (off is None) or len(off) == 0: 93 | return self.fitness.do(problem, pop, n_survive=None) 94 | 95 | # this makes it usable as a traditional survival 96 | elif isinstance(off, int) or isinstance(off, np.integer): 97 | k = off 98 | off = pop[k:] 99 | pop = pop[:k] 100 | 101 | assert len(pop) == len(off), "For the replacement pop and off must have the same number of individuals." 102 | 103 | pop = Population.create(pop) if isinstance(pop, Individual) else pop 104 | off = Population.create(off) if isinstance(off, Individual) else off 105 | 106 | I = self._do(problem, pop, off, **kwargs) 107 | 108 | if return_indices: 109 | return I 110 | else: 111 | if not inplace: 112 | pop = pop.copy() 113 | pop[I] = off[I] 114 | return self.fitness.do(problem, pop, n_survive=None) 115 | 116 | def _do( 117 | self, 118 | problem: Problem, 119 | pop: Population, 120 | off: Population, 121 | **kwargs 122 | ): 123 | return np.zeros(len(pop), dtype=np.bool_) 124 | 125 | 126 | class ImprovementReplacement(BaseReplacement): 127 | 128 | def do( 129 | self, 130 | problem: Problem, 131 | pop: Population, 132 | off: Population, 133 | return_indices=False, 134 | inplace=False, 135 | **kwargs 136 | ): 137 | """Given a problem a parent population and offspring canditates return next generation, 138 | it selects those that proceed into the next generation via one-to-one comparison. 139 | Feasible solutions are always preferred to infeasible and infeasible solutions are compared by 140 | overall constraint violation. 141 | 142 | Parameters 143 | ---------- 144 | problem : Problem 145 | Pymoo problem 146 | 147 | pop : Population 148 | Parent population 149 | 150 | off : Population | None 151 | Offspring candidates. If None, only fitness assigment is performed on parents. 152 | 153 | return_indices : bool, optional 154 | Either or not to just return boolean of positions to be replaced, by default False 155 | 156 | inplace : bool, optional 157 | Change population inplace, by default False 158 | 159 | Returns 160 | ------- 161 | Population 162 | Population that proceeds into the next generation 163 | """ 164 | return super().do(problem, pop, off, return_indices=return_indices, inplace=inplace, **kwargs) 165 | 166 | def _do( 167 | self, 168 | problem: Problem, 169 | pop: Population, 170 | off: Population, 171 | **kwargs 172 | ): 173 | 174 | ret = np.full((len(pop), 1), False) 175 | 176 | pop_F, pop_CV, pop_feas = pop.get("F", "CV", "feasible") 177 | off_F, off_CV, off_feas = off.get("F", "CV", "feasible") 178 | 179 | if problem.has_constraints() > 0: 180 | 181 | # 1) Both infeasible and constraints have been improved 182 | ret[(~pop_feas & ~off_feas) & (off_CV < pop_CV)] = True 183 | 184 | # 2) A solution became feasible 185 | ret[~pop_feas & off_feas] = True 186 | 187 | # 3) Both feasible but objective space value has improved 188 | ret[(pop_feas & off_feas) & (off_F < pop_F)] = True 189 | 190 | else: 191 | ret[off_F < pop_F] = True 192 | 193 | # never allow duplicates to become part of the population when replacement is used 194 | _, _, is_duplicate = DefaultDuplicateElimination(epsilon=0.0).do(off, pop, return_indices=True) 195 | ret[is_duplicate] = False 196 | 197 | return ret[:, 0] 198 | 199 | 200 | def parameter_less(f, cv): 201 | v = np.copy(f) 202 | infeas = cv > 0 203 | v[infeas] = f.max() + cv[infeas] 204 | return v 205 | 206 | 207 | def hierarchical_sort(f, cv=None): 208 | if cv is not None: 209 | f = parameter_less(f, cv) 210 | return np.argsort(f) 211 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "numpy>=1.19.0", "Cython>=0.29.21"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | long: tests with a longer running time. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.19.0 2 | pymoo==0.6.1 3 | scipy>=1.7.0 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from distutils.errors import CompileError 3 | import os 4 | import sys 5 | from setuptools import setup, Extension, find_packages 6 | from setuptools.command.build_ext import build_ext 7 | 8 | 9 | # --------------------------------------------------------------------------------------------------------- 10 | # BASE CONFIGURATION 11 | # --------------------------------------------------------------------------------------------------------- 12 | 13 | with open("README.md", "r", encoding="utf-8") as fh: 14 | long_description = fh.read() 15 | 16 | BASE_PACKAGE = 'pymoode' 17 | 18 | base_kwargs = dict( 19 | name = 'pymoode', 20 | packages = [BASE_PACKAGE] + [f"{BASE_PACKAGE}." + e for e in find_packages(where=BASE_PACKAGE)], 21 | version = '0.3.0', 22 | license='Apache License 2.0', 23 | description = 'A Python optimization package using Differential Evolution.', 24 | long_description=long_description, 25 | long_description_content_type="text/markdown", 26 | author = 'Bruno Scalia C. F. Leite', 27 | author_email = 'mooscaliaproject@gmail.com', 28 | url = 'https://github.com/mooscaliaproject/pymoode', 29 | download_url = 'https://github.com/mooscaliaproject/pymoode', 30 | keywords = [ 31 | 'Differential Evolution', 32 | 'Multi-objective optimization', 33 | 'GDE3', 34 | 'NSDE', 35 | 'NSDE-R', 36 | 'NSGA-II', 37 | 'Genetic Algorithm', 38 | 'Crowding Distances', 39 | 'Evolutionary Algorithms', 40 | 'Evolutionary Optimization', 41 | 'Metaheuristics', 42 | ], 43 | install_requires=[ 44 | 'numpy>=1.19.0', 45 | 'pymoo==0.6.1', 46 | 'scipy>=1.7.0', 47 | 'future', 48 | ], 49 | ) 50 | 51 | # --------------------------------------------------------------------------------------------------------- 52 | # ARGPARSER 53 | # --------------------------------------------------------------------------------------------------------- 54 | 55 | parser = argparse.ArgumentParser() 56 | 57 | parser.add_argument('--nopyx', dest='nopyx', action='store_true', help='Whether the pyx files shall be considered at all.') 58 | parser.add_argument('--nocython', dest='nocython', action='store_true', help='Whether pyx files shall be cythonized.') 59 | parser.add_argument('--nolibs', dest='nolibs', action='store_true', help='Whether the libraries should be compiled.') 60 | parser.add_argument('--markcython', dest='markcython', action='store_true', help='Whether to mark the html cython files.') 61 | 62 | params, _ = parser.parse_known_args() 63 | sys.argv = [arg for arg in sys.argv if not arg.lstrip("-") in params] 64 | 65 | # --------------------------------------------------------------------------------------------------------- 66 | # ADD MARKED HTML FILES FOR CYTHON 67 | # --------------------------------------------------------------------------------------------------------- 68 | 69 | if params.markcython: 70 | import Cython.Compiler.Options 71 | Cython.Compiler.Options.annotate = True 72 | 73 | # --------------------------------------------------------------------------------------------------------- 74 | # CLASS TO BUILD EXTENSIONS 75 | # --------------------------------------------------------------------------------------------------------- 76 | 77 | # exception that is thrown when the build fails 78 | class CompilingFailed(Exception): 79 | pass 80 | 81 | 82 | # try to compile, if not possible throw exception 83 | def construct_build_ext(build_ext): 84 | class WrappedBuildExt(build_ext): 85 | def run(self): 86 | try: 87 | build_ext.run(self) 88 | except BaseException as e: 89 | raise CompilingFailed(e) 90 | 91 | def build_extension(self, ext): 92 | try: 93 | build_ext.build_extension(self, ext) 94 | except BaseException as e: 95 | raise CompilingFailed(e) 96 | 97 | return WrappedBuildExt 98 | 99 | # --------------------------------------------------------------------------------------------------------- 100 | # HANDLING CYTHON FILES 101 | # --------------------------------------------------------------------------------------------------------- 102 | 103 | ROOT = os.path.dirname(os.path.realpath(__file__)) 104 | 105 | if params.nopyx: 106 | ext_modules = [] 107 | 108 | else: 109 | try: 110 | if params.nocython: 111 | path = os.path.join(ROOT, "pymoode", "cython") 112 | pyx = [os.path.join(path, f) for f in os.listdir() if f.endswith(".pyx")] 113 | ext_modules = [Extension(f"pymoode.cython.{source[:-4]}", [source]) for source in pyx] 114 | else: 115 | try: 116 | from Cython.Build import cythonize 117 | ext_modules = cythonize("pymoode/cython/*.pyx") 118 | except ImportError: 119 | print('*' * 75) 120 | print("No Cython package found to convert .pyx files.") 121 | print("If no compilation occurs, .py files will be used instead, which provide the same results but with worse computational time.") 122 | print('*' * 75) 123 | ext_modules = [] 124 | except: 125 | print('*' * 75) 126 | print("Problems compiling .pyx files.") 127 | print("If no compilation occurs, .py files will be used instead, which provide the same results but with worse computational time.") 128 | print('*' * 75) 129 | ext_modules = [] 130 | 131 | if not params.nolibs: 132 | 133 | if len(ext_modules) > 0: 134 | base_kwargs['ext_modules'] = ext_modules 135 | 136 | try: 137 | import numpy as np 138 | base_kwargs['include_dirs'] = [np.get_include()] 139 | 140 | except BaseException: 141 | raise CompileError( 142 | "NumPy libraries must be installed for compiled extensions! Speedups are not enabled." 143 | ) 144 | 145 | base_kwargs['cmdclass'] = dict(build_ext=construct_build_ext(build_ext)) 146 | 147 | else: 148 | print('*' * 75) 149 | print("External cython modules found.") 150 | print("To verify compilation success run:") 151 | print("from pymoode.survival.rank_and_crowding.metrics import IS_COMPILED") 152 | print("This variable will be True to mark compilation success;") 153 | print("If no compilation occurs, .py files will be used instead, which provide the same results but with worse computational time.") 154 | print('*' * 75) 155 | 156 | # --------------------------------------------------------------------------------------------------------- 157 | # RUN SETUP 158 | # --------------------------------------------------------------------------------------------------------- 159 | 160 | compiled_kwargs = base_kwargs.copy() 161 | compiled_kwargs["ext_modules"] = ext_modules 162 | 163 | try: 164 | setup(**compiled_kwargs) 165 | print('*' * 75) 166 | print("Installation successful at the first attempt.") 167 | print("To verify compilation success run:") 168 | print("from pymoode.survival.rank_and_crowding.metrics import IS_COMPILED") 169 | print('*' * 75) 170 | except: 171 | print('*' * 75) 172 | print("Running setup with cython compilation failed.") 173 | print("Attempt to a pure Python setup.") 174 | print("If no compilation occurs, .py files will be used instead, which provide the same results but with worse computational time.") 175 | print('*' * 75) 176 | setup(**base_kwargs) 177 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooscaliaproject/pymoode/521d591263ceac29d5236b3bc62a482049a18f86/tests/__init__.py -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.26.* 2 | pymoo==0.6.1 3 | scipy==1.13.* 4 | future 5 | pytest -------------------------------------------------------------------------------- /tests/test_many.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pymoo.optimize import minimize 4 | from pymoo.problems import get_problem 5 | from pymoo.indicators.igd import IGD 6 | from pymoode.algorithms import NSDER 7 | from pymoode.algorithms import GDE3 8 | from pymoode.survival import RankAndCrowding 9 | from pymoo.util.ref_dirs import get_reference_directions 10 | 11 | import numpy as np 12 | 13 | @pytest.mark.parametrize('selection', ["rand", "current-to-rand", "ranked"]) 14 | @pytest.mark.parametrize('crossover', ["bin", "exp"]) 15 | @pytest.mark.parametrize('crowding_func', ["mnn", "2nn"]) 16 | def test_many_run(selection, crossover, crowding_func): 17 | 18 | problem = get_problem("dtlz2") 19 | 20 | NGEN = 150 21 | POPSIZE = 136 22 | SEED = 5 23 | 24 | gde3 = GDE3(pop_size=POPSIZE, variant=f"DE/{selection}/1/{crossover}", CR=0.2, F=(0.0, 1.0), gamma=1e-4, 25 | survival=RankAndCrowding(crowding_func=crowding_func)) 26 | 27 | res_gde3 = minimize(problem, 28 | gde3, 29 | ('n_gen', NGEN), 30 | seed=SEED, 31 | save_history=False, 32 | verbose=False) 33 | 34 | assert len(res_gde3.opt) > 0 35 | 36 | 37 | def test_many_perf(): 38 | 39 | np.random.seed(3) 40 | assert abs(np.random.rand() - 0.5507979025745755) <= 1e-8 41 | 42 | problem = get_problem("dtlz2") 43 | ref_dirs = get_reference_directions("das-dennis", 3, n_partitions=15) 44 | igd = IGD(pf=problem.pareto_front(), zero_to_one=True) 45 | 46 | NGEN = 150 47 | POPSIZE = 136 48 | SEED = 5 49 | 50 | gde3 = GDE3(pop_size=POPSIZE, variant="DE/rand/1/bin", CR=0.2, F=(0.0, 1.0), gamma=1e-4, 51 | survival=RankAndCrowding(crowding_func="mnn")) 52 | 53 | res_gde3 = minimize(problem, 54 | gde3, 55 | ('n_gen', NGEN), 56 | seed=SEED, 57 | save_history=False, 58 | verbose=False) 59 | 60 | igd_gde3 = igd.do(res_gde3.F) 61 | assert abs(igd_gde3 - 0.04011488503871424) <= 1e-8 62 | 63 | nsder = NSDER(ref_dirs=ref_dirs, pop_size=POPSIZE, variant="DE/rand/1/bin", CR=0.5, F=(0.0, 1.0), gamma=1e-4) 64 | 65 | res_nsder = minimize(problem, 66 | nsder, 67 | ('n_gen', NGEN), 68 | seed=SEED, 69 | save_history=False, 70 | verbose=False) 71 | 72 | igd_nsder = igd.do(res_nsder.F) 73 | assert abs(igd_nsder - 0.004877000918527632) <= 1e-8 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /tests/test_multi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pymoo.optimize import minimize 4 | from pymoo.problems import get_problem 5 | from pymoo.indicators.igd import IGD 6 | from pymoode.algorithms import GDE3, NSDE 7 | from pymoode.operators.dex import DEX 8 | from pymoode.operators.dem import DEM 9 | from pymoo.algorithms.moo.nsga2 import NSGA2 10 | from pymoo.operators.mutation.pm import PM 11 | from pymoode.survival import RankAndCrowding, ConstrRankAndCrowding 12 | 13 | 14 | @pytest.mark.parametrize('survival', [RankAndCrowding, ConstrRankAndCrowding]) 15 | @pytest.mark.parametrize('crowding_func', ["mnn", "2nn", "cd", "pcd", "ce"]) 16 | def test_multi_run(survival, crowding_func): 17 | 18 | problem = get_problem("truss2d") 19 | 20 | NGEN = 50 21 | POPSIZE = 50 22 | SEED = 5 23 | 24 | gde3 = GDE3(pop_size=POPSIZE, variant="DE/rand/1/bin", CR=0.5, F=(0.0, 0.9), de_repair="bounce-back", 25 | survival=survival(crowding_func=crowding_func)) 26 | 27 | res_gde3 = minimize(problem, 28 | gde3, 29 | ('n_gen', NGEN), 30 | seed=SEED, 31 | save_history=False, 32 | verbose=False) 33 | 34 | assert len(res_gde3.opt) > 0 35 | 36 | 37 | @pytest.mark.parametrize('crossover', [DEX(), DEM()]) 38 | def test_multi_frankstein(crossover): 39 | 40 | problem = get_problem("truss2d") 41 | 42 | NGEN = 100 43 | POPSIZE = 100 44 | SEED = 5 45 | 46 | frank = NSGA2(pop_size=POPSIZE, crossover=crossover) 47 | 48 | res_frank = minimize(problem, 49 | frank, 50 | ('n_gen', NGEN), 51 | seed=SEED, 52 | save_history=False, 53 | verbose=False) 54 | 55 | assert len(res_frank.opt) > 0 56 | 57 | 58 | def test_gde3_pm_run(): 59 | 60 | problem = get_problem("truss2d") 61 | 62 | NGEN = 50 63 | POPSIZE = 50 64 | SEED = 5 65 | 66 | gde3pm = GDE3(pop_size=POPSIZE, variant="DE/rand/1/bin", CR=0.5, F=(0.0, 0.9), de_repair="bounce-back", 67 | survival=RankAndCrowding(crowding_func="pcd"), genetic_mutation=PM()) 68 | 69 | res_gde3pm = minimize(problem, 70 | gde3pm, 71 | ('n_gen', NGEN), 72 | seed=SEED, 73 | save_history=False, 74 | verbose=False) 75 | 76 | assert len(res_gde3pm.opt) > 0 77 | 78 | gde3 = GDE3(pop_size=POPSIZE, variant="DE/rand/1/bin", CR=0.5, F=(0.0, 0.9), de_repair="bounce-back", 79 | survival=RankAndCrowding(crowding_func="pcd")) 80 | 81 | res_gde3 = minimize(problem, 82 | gde3, 83 | ('n_gen', NGEN), 84 | seed=SEED, 85 | save_history=False, 86 | verbose=False) 87 | 88 | assert len(sum(res_gde3pm.F - res_gde3.F)) >= 1e-3 89 | 90 | 91 | def test_multi_perf(): 92 | 93 | problem = get_problem("truss2d") 94 | igd = IGD(pf=problem.pareto_front(), zero_to_one=True) 95 | 96 | NGEN = 250 97 | POPSIZE = 100 98 | SEED = 5 99 | 100 | gde3 = GDE3(pop_size=POPSIZE, variant="DE/rand/1/bin", CR=0.5, F=(0.0, 0.9), de_repair="bounce-back", 101 | survival=RankAndCrowding(crowding_func="cd")) 102 | 103 | res_gde3 = minimize(problem, 104 | gde3, 105 | ('n_gen', NGEN), 106 | seed=SEED, 107 | save_history=False, 108 | verbose=False) 109 | 110 | igd_gde3 = igd.do(res_gde3.F) 111 | assert abs(igd_gde3 - 0.005859828655308572) <= 1e-8 112 | 113 | gde3p = GDE3(pop_size=POPSIZE, variant="DE/rand/1/bin", CR=0.5, F=(0.0, 0.9), de_repair="bounce-back", 114 | survival=RankAndCrowding(crowding_func="pcd")) 115 | 116 | res_gde3p = minimize(problem, 117 | gde3p, 118 | ('n_gen', NGEN), 119 | seed=SEED, 120 | save_history=False, 121 | verbose=False) 122 | 123 | igd_gde3p = igd.do(res_gde3p.F) 124 | assert abs(igd_gde3p - 0.004744463013355145) <= 1e-8 125 | 126 | nsde = NSDE(pop_size=POPSIZE, variant="DE/rand/1/bin", CR=0.5, F=(0.0, 0.9), de_repair="bounce-back", 127 | survival=RankAndCrowding(crowding_func="pcd")) 128 | 129 | res_nsde = minimize(problem, 130 | nsde, 131 | ('n_gen', NGEN), 132 | seed=SEED, 133 | save_history=False, 134 | verbose=False) 135 | 136 | igd_nsde = igd.do(res_nsde.F) 137 | assert abs(igd_nsde - 0.004562068055351625) <= 1e-8 -------------------------------------------------------------------------------- /tests/test_single.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pymoo.optimize import minimize 4 | from pymoo.problems import get_problem 5 | from pymoo.operators.mutation.pm import PM 6 | from pymoode.algorithms import GDE3 7 | from pymoode.algorithms import DE 8 | 9 | 10 | @pytest.mark.parametrize('selection', ["rand", "best", "current-to-best", "current-to-rand", "ranked"]) 11 | @pytest.mark.parametrize('crossover', ["bin", "exp"]) 12 | @pytest.mark.parametrize('de_repair', ["bounce-back", "midway", "rand-init", "to-bounds"]) 13 | def test_de_run(selection, crossover, de_repair): 14 | problem = get_problem("rastrigin") 15 | 16 | NGEN = 50 17 | POPSIZE = 20 18 | SEED = 3 19 | 20 | #DE Parameters 21 | CR = 0.5 22 | F = (0.3, 1.0) 23 | 24 | de = DE(pop_size=POPSIZE, variant=f"DE/{selection}/1/{crossover}", CR=CR, F=F, de_repair=de_repair) 25 | 26 | res_de = minimize(problem, 27 | de, 28 | ('n_gen', NGEN), 29 | seed=SEED, 30 | save_history=False, 31 | verbose=False) 32 | 33 | assert len(res_de.opt) > 0 34 | 35 | 36 | def test_de_pm_run(): 37 | 38 | problem = get_problem("rastrigin") 39 | 40 | NGEN = 50 41 | POPSIZE = 20 42 | SEED = 3 43 | 44 | #DE Parameters 45 | CR = 0.5 46 | F = (0.3, 1.0) 47 | 48 | de = DE(pop_size=POPSIZE, variant=f"DE/rand/1/bin", CR=CR, F=F) 49 | 50 | res_de = minimize(problem, 51 | de, 52 | ('n_gen', NGEN), 53 | seed=SEED, 54 | save_history=False, 55 | verbose=False) 56 | 57 | depm = DE(pop_size=POPSIZE, variant=f"DE/rand/1/bin", CR=CR, F=F, genetic_mutation=PM()) 58 | 59 | res_pm = minimize(problem, 60 | depm, 61 | ('n_gen', NGEN), 62 | seed=SEED, 63 | save_history=False, 64 | verbose=False) 65 | 66 | assert len(res_pm.opt) > 0 67 | assert len(sum(res_de.pop.get("F") - res_pm.pop.get("F"))) >= 1e-6 68 | 69 | 70 | def test_de_perf(): 71 | problem = get_problem("rastrigin") 72 | 73 | NGEN = 100 74 | POPSIZE = 20 75 | SEED = 3 76 | 77 | #DE Parameters 78 | CR = 0.5 79 | F = (0.3, 1.0) 80 | 81 | de = DE(pop_size=POPSIZE, variant="DE/rand/1/bin", CR=CR, F=F) 82 | 83 | res_de = minimize(problem, 84 | de, 85 | ('n_gen', NGEN), 86 | seed=SEED, 87 | save_history=False, 88 | verbose=False) 89 | 90 | assert len(res_de.opt) > 0 91 | assert res_de.F <= 1e-6 92 | 93 | gde3 = GDE3(pop_size=POPSIZE, variant="DE/rand/1/bin", CR=CR, F=F) 94 | 95 | res_gde3 = minimize(problem, 96 | gde3, 97 | ('n_gen', NGEN), 98 | seed=SEED, 99 | save_history=False, 100 | verbose=False) 101 | 102 | assert res_gde3.F <= 1e-6 103 | 104 | -------------------------------------------------------------------------------- /tests/test_spacing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import numpy as np 4 | from pymoo.optimize import minimize 5 | from pymoo.problems import get_problem 6 | from pymoode.performance import SpacingIndicator 7 | from pymoode.algorithms import GDE3 8 | from pymoode.survival import RankAndCrowding 9 | 10 | 11 | def test_spacing_values(): 12 | 13 | F = np.array([ 14 | [1.2, 7.8], 15 | [2.8, 5.1], 16 | [4.0, 2.8], 17 | [7.0, 2.2], 18 | [8.4, 1.2] 19 | ]) 20 | 21 | assert abs(SpacingIndicator().do(F) - 0.73) <= 1e-3 22 | 23 | F = np.array([ 24 | [1.0, 7.5], 25 | [1.1, 5.5], 26 | [2.0, 5.0], 27 | [3.0, 4.0], 28 | [4.0, 2.8], 29 | [5.5, 2.5], 30 | [6.8, 2.0], 31 | [8.4, 1.2] 32 | ]) 33 | 34 | assert abs(SpacingIndicator().do(F) - 0.316) <= 1e-3 35 | 36 | 37 | def test_spacing_zero_to_one(): 38 | 39 | problem = get_problem("truss2d") 40 | sp_from_pf = SpacingIndicator(pf=problem.pareto_front(), zero_to_one=True) 41 | sp_from_ref_points = SpacingIndicator(zero_to_one=True, nadir=problem.nadir_point(), ideal=problem.ideal_point()) 42 | sp_no_scale = SpacingIndicator(zero_to_one=False) 43 | 44 | NGEN = 250 45 | POPSIZE = 100 46 | SEED = 5 47 | 48 | gde3 = GDE3(pop_size=POPSIZE, variant="DE/rand/1/bin", CR=0.5, F=(0.0, 0.9), de_repair="bounce-back", 49 | survival=RankAndCrowding(crowding_func="pcd")) 50 | 51 | res_gde3 = minimize(problem, 52 | gde3, 53 | ('n_gen', NGEN), 54 | seed=SEED, 55 | save_history=False, 56 | verbose=False) 57 | 58 | assert sp_from_pf.do(res_gde3.F) > 0 59 | assert sp_from_ref_points.do(res_gde3.F) > 0 60 | assert sp_no_scale.do(res_gde3.F) > 0 61 | assert abs(sp_from_pf.do(res_gde3.F) - sp_from_ref_points.do(res_gde3.F)) <= 1e-6 --------------------------------------------------------------------------------