├── ressources ├── f1_apres.png ├── f1_avant.png ├── f2_apres.png ├── f2_avant.png └── wb2s_vs_ehvi.png ├── smoot ├── __init__.py ├── montecarlo.py ├── zdt.py ├── utils.py ├── criterion.py └── smoot.py ├── setup.py ├── .github └── workflows │ └── tests.yml ├── tests ├── test_zdt.py └── test_moo.py ├── README.md ├── LICENSE └── .gitignore /ressources/f1_apres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onera/smoot/HEAD/ressources/f1_apres.png -------------------------------------------------------------------------------- /ressources/f1_avant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onera/smoot/HEAD/ressources/f1_avant.png -------------------------------------------------------------------------------- /ressources/f2_apres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onera/smoot/HEAD/ressources/f2_apres.png -------------------------------------------------------------------------------- /ressources/f2_avant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onera/smoot/HEAD/ressources/f2_avant.png -------------------------------------------------------------------------------- /ressources/wb2s_vs_ehvi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onera/smoot/HEAD/ressources/wb2s_vs_ehvi.png -------------------------------------------------------------------------------- /smoot/__init__.py: -------------------------------------------------------------------------------- 1 | from .smoot import MOO 2 | from .zdt import ZDT 3 | from .utils import ( 4 | write_results, 5 | read_results, 6 | pymoo2fun, 7 | pymoo2constr, 8 | write_increase_iter, 9 | ) 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="smoot", 8 | version="0.2.0", 9 | author="Nathalie Bartoli, Robin Grapin, Rémi Lafage", 10 | description="Surrogate based multi-objective optimization tool", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | license="BSD-3", 14 | url="https://github.com/OneraHub/smoot", 15 | classifiers=[ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: BSD License", 18 | "Operating System :: OS Independent", 19 | ], 20 | packages=["smoot"], 21 | python_requires=">=3.7", 22 | install_requires=["smt", "pymoo>=0.6.0"], 23 | ) 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | python-version: [3.7, 3.8, 3.9] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | python -m pip install black pytest 28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | pip install . 30 | - name: Format with black 31 | run: | 32 | black --check . 33 | - name: Test with pytest 34 | run: | 35 | pytest 36 | -------------------------------------------------------------------------------- /tests/test_zdt.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: robin grapin 3 | 4 | This package is distributed under New BSD license. 5 | """ 6 | import numpy as np 7 | import unittest 8 | from smt.utils.sm_test_case import SMTestCase 9 | from smoot import ZDT 10 | 11 | 12 | class Test(SMTestCase): 13 | def run_test(self, problem): 14 | problem.options["return_complex"] = True 15 | 16 | # Test xlimits 17 | ndim = problem.options["ndim"] 18 | xlimits = problem.xlimits 19 | self.assertEqual(xlimits.shape, (ndim, 2)) 20 | 21 | # Test evaluation of multiple points at once 22 | x = np.zeros((10, ndim)) 23 | for ind in range(10): 24 | x[ind, :] = 0.5 * (xlimits[:, 0] + xlimits[:, 1]) 25 | y = problem(x) 26 | if type(y) == list or type(y) == tuple: 27 | y = y[0] 28 | self.assertEqual(x.shape[0], y.shape[0]) 29 | 30 | def test_zdt(self): 31 | self.run_test(ZDT(type=1)) 32 | self.run_test(ZDT(type=2)) 33 | self.run_test(ZDT(type=3)) 34 | self.run_test(ZDT(type=4)) 35 | self.run_test(ZDT(type=5)) 36 | 37 | 38 | if __name__ == "__main__": 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tests](https://github.com/OneraHub/smoot/workflows/Tests/badge.svg)](https://github.com/OneraHub/smoot/actions?query=workflow%3ATests) 2 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 3 | 4 | # smoot 5 | 6 | ## Installation 7 | 8 | ONERA version : 9 | 10 | pip install smoot 11 | 12 | 13 | Required packages : pymoo,smt 14 | 15 | ## Description 16 | 17 | This surrogate based multi-objective Bayesian optimizer has been created to see the performance of the WB2S criterion adapted to multi-objective problems. 18 | Given a black box function f : **x** -> **y** with **bolds** characters as vectors, smoot will give an accurate approximation of the optima with few calls of f. 19 | 20 | ![modeli1](ressources/f1_avant.png) 21 | ![modeli2](ressources/f2_avant.png) 22 | 23 | ![activ](ressources/wb2s_vs_ehvi.png) 24 | 25 | ![modeli12](ressources/f1_apres.png) 26 | ![modeli22](ressources/f2_apres.png) 27 | 28 | ### Usage 29 | 30 | Look at the Jupyter notebook in the *tutorial* folder. 31 | 32 | You will learn how to use implemented the functionnalities and options such as : 33 | - The choice of the infill criterion 34 | - The method to manage the constraints 35 | 36 | For additional questions, contact: robingrapin@orange.fr 37 | -------------------------------------------------------------------------------- /smoot/montecarlo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon May 3 14:38:58 2021 4 | 5 | @author: robin 6 | """ 7 | 8 | import numpy as np 9 | 10 | 11 | class MonteCarlo(object): 12 | def __init__(self, random_state=None): 13 | self.seed = np.random.RandomState(random_state) 14 | 15 | def sampling(self, x, distrib, points=300): 16 | """ 17 | Samples the objective space according to the probability distribution 18 | Points are uniformly generated on the design space, then their image 19 | through the model is the output 20 | 21 | Parameters 22 | ---------- 23 | x : ndarray[n_dim] 24 | Design point to evaluate thanks to a criteria after the sampling. 25 | distrib : list of smt models 26 | models of the objective. 27 | points : int, optional 28 | Number of points of the sampling. Should be modulated in function of the number of objectives. The default is 300. 29 | 30 | Returns 31 | ------- 32 | ndarray[points, n_obj] 33 | point's distribution in the objective space according to the model(s). 34 | 35 | """ 36 | moyennes = np.asarray([model.predict_values(x)[0][0] for model in distrib]) 37 | sigmas = np.asarray( 38 | [model.predict_variances(x)[0][0] ** 0.5 for model in distrib] 39 | ) 40 | return self.seed.normal(moyennes, sigmas, (points, len(distrib))) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, OneraHub 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /tests/test_moo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Fri Apr 16 13:12:07 2021 4 | 5 | @author: robin grapin 6 | """ 7 | import warnings 8 | 9 | warnings.filterwarnings("ignore") 10 | 11 | import time 12 | import unittest 13 | import numpy as np 14 | 15 | from smoot.smoot import MOO 16 | from smoot.zdt import ZDT 17 | 18 | from smt.sampling_methods import LHS 19 | from smt.problems import Branin 20 | from smt.utils.sm_test_case import SMTestCase 21 | 22 | from pymoo.indicators.gd import GD 23 | 24 | 25 | class TestMOO(SMTestCase): 26 | def test_Branin(self): 27 | n_iter = 5 28 | fun = Branin() 29 | criterion = "EI" 30 | 31 | mo = MOO( 32 | n_iter=n_iter, 33 | criterion=criterion, 34 | xlimits=fun.xlimits, 35 | random_state=42, 36 | ) 37 | print("running test Branin 2D -> 1D") 38 | start = time.time() 39 | mo.optimize(fun=fun) 40 | x_opt, y_opt = mo.result.X[0][0], mo.result.F[0][0] 41 | print("x_opt :", x_opt) 42 | print("y_opt :", y_opt) 43 | print("seconds taken Branin: ", time.time() - start, "\n") 44 | self.assertTrue( 45 | np.allclose([[-3.14, 12.275]], x_opt, rtol=0.5) 46 | or np.allclose([[3.14, 2.275]], x_opt, rtol=0.5) 47 | or np.allclose([[9.42, 2.475]], x_opt, rtol=0.5) 48 | ) 49 | self.assertAlmostEqual(0.39, float(y_opt), delta=1) 50 | 51 | def test_zdt(self, type=1, criterion="EHVI", ndim=2, n_iter=5): 52 | fun = ZDT(type=type, ndim=ndim) 53 | 54 | mo = MOO( 55 | n_iter=n_iter, 56 | criterion=criterion, 57 | random_state=1, 58 | ) 59 | print("running test ZDT", type, ": " + str(ndim) + "D -> 2D,", criterion) 60 | start = time.time() 61 | mo.optimize(fun=fun) 62 | print("seconds taken :", time.time() - start) 63 | exact = fun.pareto(random_state=1)[1] 64 | gd = GD(exact) 65 | dist = gd(mo.result.F) 66 | print("distance to the exact Pareto front", dist, "\n") 67 | self.assertLess(dist, 2.5) 68 | 69 | def test_zdt_2(self): 70 | self.test_zdt(type=2, criterion="WB2S") 71 | 72 | def test_zdt_3(self): 73 | self.test_zdt(type=3, criterion="PI") 74 | 75 | def test_zdt_2_3Dto2D(self): 76 | self.test_zdt(type=2, criterion="EHVI", ndim=3) 77 | 78 | def test_train_pts_known(self): 79 | fun = ZDT() 80 | xlimits = fun.xlimits 81 | sampling = LHS(xlimits=xlimits, random_state=42) 82 | xt = sampling(20) # generating data as if it were known data 83 | yt = fun(xt) # idem : "known" datapoint for training 84 | mo = MOO(n_iter=5, criterion="MPI", xdoe=xt, ydoe=yt, random_state=42) 85 | print("running test ZDT with known training points") 86 | start = time.time() 87 | mo.optimize(fun=fun) 88 | print("seconds taken :", time.time() - start) 89 | exact = fun.pareto(random_state=1)[1] 90 | gd = GD(exact) 91 | dist = gd(mo.result.F) 92 | print("distance to the exact Pareto front", dist, "\n") 93 | self.assertLess(dist, 2.5) 94 | 95 | 96 | if __name__ == "__main__": 97 | unittest.main() 98 | -------------------------------------------------------------------------------- /smoot/zdt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Apr 6 10:07:51 2021 4 | 5 | @author: robin 6 | """ 7 | 8 | import numpy as np 9 | from smt.problems.problem import Problem 10 | 11 | 12 | class ZDT(Problem): 13 | """ 14 | ZDT toolkit 15 | x = {x1.. xn} 16 | y = {x1.. xj} = {y1.. yj} 17 | z = {x(j+1).. xn} = {z1.. zk} 18 | Testing functions with the shape : 19 | f1 : y -> f1(y) 20 | f2 : y,z -> g(z)h(f1(y),g(z)) 21 | xbounds = [0,1]**n 22 | """ 23 | 24 | def _initialize(self): 25 | self.options.declare("ndim", 2, types=int) 26 | self.options.declare("name", "ZDT", types=str) 27 | self.options.declare( 28 | "type", 1, values=[1, 2, 3, 4, 5], types=int 29 | ) # one of the 5 test functions 30 | 31 | def _setup(self): 32 | self.xlimits[:, 1] = 1.0 33 | 34 | def _evaluate(self, x, kx=None): 35 | """ 36 | Arguments 37 | --------- 38 | x : ndarray[ne, n_dim] 39 | Evaluation points. 40 | 41 | Returns 42 | ------- 43 | [ndarray[ne, 1],ndarray[ne, 1]] 44 | Functions values. 45 | """ 46 | ne, nx = x.shape 47 | j = min(1, nx - 1) # if one entry then no bug 48 | f1 = np.zeros((ne, 1)) 49 | 50 | if self.options["type"] < 5: 51 | f1[:, 0] = x[:, 0] 52 | else: 53 | f1[:, 0] = 1 - np.exp(-4 * x[:, 0]) * np.sin(6 * np.pi * x[:, 0]) ** 6 54 | 55 | # g 56 | g = np.zeros((ne, 1)) 57 | if self.options["type"] < 4: 58 | for i in range(ne): 59 | g[i, 0] = 1 + 9 / (nx - j) * sum(x[i, j:nx]) 60 | elif self.options["type"] == 4: 61 | for i in range(ne): 62 | g[i, 0] = ( 63 | 1 64 | + 10 * (nx - j) 65 | + sum(x[i, j:nx] ** 2 - 10 * np.cos(4 * np.pi * x[i, j + 1 : nx])) 66 | ) 67 | else: 68 | for i in range(ne): 69 | g[i, 0] = 1 + 9 * (sum(x[i, j:nx]) / (nx - j)) ** 0.25 70 | 71 | # h 72 | h = np.zeros((ne, 1)) 73 | if self.options["type"] == 1 or self.options["type"] == 4: 74 | for i in range(ne): 75 | h[i, 0] = 1 - np.sqrt(f1[i, 0] / g[i, 0]) 76 | elif self.options["type"] == 2 or self.options["type"] == 5: 77 | for i in range(ne): 78 | h[i, 0] = 1 - (f1[i, 0] / g[i, 0]) ** 2 79 | else: 80 | for i in range(ne): 81 | h[i, 0] = ( 82 | 1 83 | - np.sqrt(f1[i, 0] / g[i, 0]) 84 | - f1[i, 0] / g[i, 0] * np.sin(10 * np.pi * f1[i, 0]) 85 | ) 86 | 87 | return np.hstack((f1, g * h)) 88 | 89 | def pareto(self, npoints=300, random_state=None): 90 | """ 91 | Give points of the pareto set and front, useful for plots and 92 | solver's quality comparition. Pareto reached when g = 0 in ZDT, 93 | what means that only x1 is not null. 94 | 95 | Parameters 96 | ---------- 97 | npoints : int, optional 98 | NUmber of points to generate. The default is 300. 99 | 100 | Returns 101 | ------- 102 | X,Y : ndarray[npoints, ndim] , [ndarray[npoints, 1],ndarray[npoints, 1]] 103 | X are points from the pareto set, Y their values. 104 | 105 | """ 106 | rand = np.random.RandomState(random_state) 107 | X = np.zeros((npoints, self.options["ndim"])) 108 | if self.options["type"] == 3: 109 | F = [ 110 | [0, 0.0830015349], 111 | [0.4093136748, 0.4538821041], 112 | [0.6183967944, 0.6525117038], 113 | [0.8233317983, 0.8518328654], 114 | ] 115 | b1 = F[0][1] 116 | b2 = b1 + F[1][1] - F[1][0] 117 | b3 = b2 + F[2][1] - F[2][0] 118 | b4 = b3 + F[3][1] - F[3][0] # sum([inter[1]-inter[0] for inter in F ]) 119 | for i in range(npoints): 120 | pt = rand.uniform(0, b4) 121 | if pt > b3: 122 | X[i, 0] = F[3][0] + pt - b3 123 | elif pt > b2: 124 | X[i, 0] = F[2][0] + pt - b2 125 | elif pt > b1: 126 | X[i, 0] = F[1][0] + pt - b1 127 | else: 128 | X[i, 0] = pt 129 | else: 130 | X[:, 0] = rand.uniform(0, 1, npoints) 131 | return X, self._evaluate(X) 132 | -------------------------------------------------------------------------------- /smoot/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon May 31 09:35:47 2021 4 | 5 | @author: robin 6 | """ 7 | 8 | from smoot import MOO 9 | from smoot import ZDT 10 | 11 | import ast 12 | import matplotlib.pyplot as plt 13 | import numpy as np 14 | import pickle 15 | import time 16 | from smt.sampling_methods import LHS 17 | from pymoo.factory import get_performance_indicator 18 | 19 | 20 | def write_increase_iter( 21 | fun, 22 | path, 23 | xlimits=None, 24 | reference=None, 25 | n_max=10, 26 | runs=5, 27 | paraMOO={}, 28 | verbose=True, 29 | indic="igd", 30 | start_seed=0, 31 | criterions=["PI", "EHVI", "GA", "WB2S", "MPI"], 32 | subcrits=["EHVI", "EHVI", "EHVI", "EHVI", "EHVI"], 33 | transfos=[ 34 | lambda l: sum(l), 35 | lambda l: sum(l), 36 | lambda l: sum(l), 37 | lambda l: sum(l), 38 | lambda l: sum(l), 39 | ], 40 | titles=None, 41 | ): 42 | """ 43 | write a dictionnary with the results of the runs for each criterion in path. 44 | 45 | Parameters 46 | ---------- 47 | fun : function 48 | function to optimize. 49 | path : str 50 | path to store datas. 51 | xlimits : ndarray[n_var,2], if None then it takes fun.xlimits 52 | bounds limits of fun, optional 53 | reference : ndarray[ n_points, n_obj], optional 54 | comparison explicit pareto points or reference point if "hv" is the indicator. The default is None. 55 | n_max : int 56 | maximal number of points added during the enrichent process. The default is 10. 57 | runs : int, optional 58 | number of runs with different seeds. The default is 5. 59 | paraMOO : dictionnary, optional 60 | Non-default MOO parameters for the optimization. The default is {"pop_size" : 50}. 61 | verbose : Bool, optional 62 | If informations are given during the process. The default is True. 63 | subcrits : list of str 64 | Subcriterions for wb2S 65 | transfos : list of function 66 | Transformations for wb2S 67 | """ 68 | if xlimits is None: 69 | xlimits = fun.xlimits 70 | if reference is None and indic != "hv": 71 | reference = fun.pareto()[1] 72 | if indic == "hv": 73 | igd = get_performance_indicator(indic, ref_point=reference) 74 | else: 75 | igd = get_performance_indicator(indic, reference) 76 | if titles is None: 77 | titles = criterions 78 | fichier = open(path, "wb") 79 | mo = MOO(xlimits=xlimits) 80 | for clef, val in paraMOO.items(): 81 | mo.options._dict[clef] = val 82 | 83 | def obj_profile( 84 | criterion="PI", n=n_max, seed=3, subcrit="EHVI", transfo=lambda l: sum(l) 85 | ): 86 | """ 87 | Intermediate function to run for a specific criterion 1 run 88 | of MOO, giving the resulting front at each iteration 89 | """ 90 | fronts = [] 91 | times = [] 92 | sampling = LHS(xlimits=xlimits, random_state=seed) 93 | xdoe = sampling(mo.options["n_start"]) 94 | mo.options["criterion"] = criterion 95 | mo.options["random_state"] = seed 96 | mo.options["xdoe"] = xdoe 97 | mo.options["n_iter"] = 0 98 | mo.options["subcrit"] = subcrit 99 | mo.options["transfo"] = transfo 100 | mo.optimize(fun) 101 | fronts.append(mo.result.F) 102 | mo.options["n_iter"] = 1 103 | for i in range(1, n): 104 | stime = time.time() 105 | mo.options[ 106 | "xdoe" 107 | ] = xdoe # the last doe is used for the new iteration to add a point 108 | X, _ = mo.optimize(fun) 109 | fronts.append(fun(X)) 110 | times.append(time.time() - stime) 111 | xdoe = mo.modeles[0].training_points[None][0][0] 112 | dists = [igd.calc(fr) for fr in fronts] # - to have the growth as goal 113 | 114 | if verbose: 115 | print("xdoe", xdoe) 116 | # print("distances",dists) 117 | return dists, fronts, times 118 | 119 | dico_res = {crit: {} for crit in titles} 120 | # plots_moy = [] 121 | for i, crit in enumerate(criterions): 122 | fronts_pareto = [] 123 | distances = [] 124 | temps = [] 125 | if verbose: 126 | print("criterion ", titles[i]) 127 | for graine in range(start_seed, start_seed + runs): 128 | if verbose: 129 | print("seed", graine) 130 | di, fr, tmps = obj_profile( 131 | crit, n=n_max, seed=graine, subcrit=subcrits[i], transfo=transfos[i] 132 | ) 133 | fronts_pareto.append(fr) 134 | distances.append(di) 135 | temps.append(tmps) 136 | dico_res[titles[i]] = { 137 | "time": temps.copy(), 138 | "fronts": fronts_pareto.copy(), 139 | "dists": distances.copy(), 140 | } 141 | pickle.dump(dico_res, fichier) 142 | fichier.close() 143 | 144 | 145 | def write_results(fun, path, runs=1, paraMOO={}): 146 | """ 147 | Run runs times the optimizer on fun using the paraMOO parameters. 148 | The results of each run are stored in path using the pickle module. 149 | To get the datas for postprocessing, use read_results(path). 150 | 151 | Parameters 152 | ---------- 153 | fun : function 154 | black box function to optimize : ndarray[ne,nx] -> ndarray[ne,ny] 155 | path : string 156 | Absolute path to store the datas. 157 | runs : int, optional 158 | Number of runs of the MOO solver. The default is 1. 159 | paraMOO : dictionnary, optional 160 | parameters for MOO solver. The default is {}. 161 | """ 162 | fichier = open(path, "wb") 163 | mo = MOO() 164 | for clef, val in paraMOO.items(): 165 | mo.options._dict[clef] = val 166 | pickle.dump(mo.options._dict, fichier) 167 | dico_res = {} 168 | for i in range(runs): 169 | titre = "run" + str(i) 170 | dico_res[titre] = {} 171 | mo.optimize(fun) 172 | dico_res[titre]["F"] = mo.result.F 173 | dico_res[titre]["X"] = mo.result.X 174 | pickle.dump(dico_res, fichier) 175 | fichier.close() 176 | 177 | 178 | def read_results(path): 179 | """ 180 | read the results written thanks to write_results in path 181 | 182 | Parameters 183 | ---------- 184 | path : string 185 | Absolute path. 186 | 187 | Returns 188 | ------- 189 | param : dictionnary 190 | dictionnary of the given parameters for the runs 191 | results : dictionnary 192 | contains the datas relatives to the runs. For instance, 193 | results["run0"]["F"] contains the pareto front of the first run. 194 | """ 195 | fichier = open(path, "rb") 196 | param = pickle.load(fichier) 197 | results = pickle.load(fichier) 198 | fichier.close() 199 | return param, results 200 | 201 | 202 | def pymoo2fun(pb): 203 | """ 204 | Takes a pymoo problem and makes of it a funtion optimizable thanks to MOO 205 | 206 | Parameters 207 | ---------- 208 | pb : pymoo.problems 209 | Pymoo problem, such as those obtained thanks to get_problem from pymoo.factory. 210 | 211 | Returns 212 | ------- 213 | f_equiv : function 214 | Callable function, equivalent to the one of the problem : ndarray[ne,nx] -> ndarray[ne,ny]. 215 | 216 | """ 217 | 218 | def f_equiv(x): 219 | output = {} 220 | pb._evaluate(x, output) 221 | return output["F"] 222 | 223 | return f_equiv 224 | 225 | 226 | def pymoo2constr(pb): 227 | """ 228 | Creates the list of the constraints relatives to the pymoo problem in argument. 229 | 230 | Parameters 231 | ---------- 232 | pb : pymoo.problems 233 | Constrained pymoo problem to optimize. 234 | 235 | Returns 236 | ------- 237 | list_con : list 238 | List of the callable constraints : ndarray[ne,nx] -> ndarray[ne]. 239 | 240 | """ 241 | list_con = [] 242 | for i in range(pb.n_constr): 243 | 244 | def g_equiv(x, i=i): 245 | output = {} 246 | pb._evaluate(x, output) 247 | return output["G"][:, i] 248 | 249 | list_con.append(g_equiv) 250 | return list_con 251 | -------------------------------------------------------------------------------- /smoot/criterion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Apr 26 10:26:43 2021 4 | 5 | @author: Robin Grapin 6 | """ 7 | import numpy as np 8 | from scipy.stats import norm 9 | from smoot.montecarlo import MonteCarlo 10 | 11 | 12 | class Criterion(object): 13 | def __init__( 14 | self, 15 | name, 16 | models, 17 | ref=None, 18 | s=None, 19 | hv=None, 20 | random_state=None, 21 | subcrit=None, 22 | transfo=None, 23 | ): 24 | self.models = models 25 | self.name = name 26 | self.ref = ref 27 | self.s = s 28 | self.hv = hv 29 | self.points = 100 * len(models) # maybe 100 too slow ? 30 | self.random_state = random_state 31 | self.subcrit = subcrit 32 | self.transfo = transfo 33 | 34 | def __call__(self, x, means=None, variances=None, pareto_front=None): 35 | if self.name == "PI": 36 | return self.PI(x) 37 | if self.name == "EHVI": 38 | return self.EHVI(x) 39 | if self.name == "HV": 40 | return self.HV(x) 41 | if self.name == "WB2S": 42 | return self.WB2S(x) 43 | if self.name == "MPI": 44 | return self.MPI(x) 45 | 46 | def MPI(self, x): 47 | """ 48 | Minimal Porbability of Improvement 49 | 50 | Parameters 51 | ---------- 52 | x : list 53 | coordinate in the design point to evaluate. 54 | 55 | Returns 56 | ------- 57 | float 58 | MPI(x). 59 | """ 60 | x = np.asarray(x).reshape(1, -1) 61 | 62 | pf = Criterion._compute_pareto(self.models) 63 | variances = [mod.predict_variances for mod in self.models] 64 | etypes = [var(x)[0][0] ** 0.5 for var in variances] 65 | if 0 in etypes: # training point 66 | return 0 67 | moyennes = [mod.predict_values for mod in self.models] 68 | moy = [m(x)[0][0] for m in moyennes] 69 | probas = [ 70 | np.prod( 71 | [norm.cdf((moy[i] - f[i]) / etypes[i]) for i in range(len(moyennes))] 72 | ) 73 | for f in pf 74 | ] 75 | 76 | return 1 - max(probas) # min( 1 - P ) 77 | 78 | def PI(self, x): 79 | """ 80 | Probability of improvement of the point x for 2 objectives. 81 | If more than 2 objectives, computed using Monte-Carlo sampling instead 82 | 83 | Parameters 84 | ---------- 85 | x : list 86 | coordinates in the design space of the point to evaluate. 87 | 88 | Returns 89 | ------- 90 | pi_x : float 91 | PI(x) : probability that x is an improvement € [0,1] 92 | """ 93 | 94 | x = np.asarray(x).reshape(1, -1) 95 | pareto_front = Criterion._compute_pareto(self.models) 96 | 97 | if len(self.models) > 2: 98 | y = np.asarray( 99 | [ 100 | mod.predict_values(x)[0][0] 101 | - 3 * mod.predict_variances(x)[0][0] ** 0.5 102 | for mod in self.models 103 | ] 104 | ) 105 | if Criterion.is_dominated(y, pareto_front): 106 | return 0 # the point - 3sigma is dominated, almost no chances of improvement 107 | MC = MonteCarlo(random_state=self.random_state) 108 | q = MC.sampling(x, self.models, self.points) 109 | return ( 110 | self.points 111 | - sum([Criterion.is_dominated(qi, pareto_front) for qi in q]) 112 | ) / self.points # maybe we can remove the division by self.points as there is the same amount of points for each call? It's just for scale here 113 | 114 | pareto_front.sort(key=lambda x: x[0]) 115 | moyennes = [mod.predict_values for mod in self.models] 116 | variances = [mod.predict_variances for mod in self.models] 117 | sig1, sig2 = variances[0](x)[0][0] ** 0.5, variances[1](x)[0][0] ** 0.5 118 | moy1, moy2 = moyennes[0](x)[0][0], moyennes[1](x)[0][0] 119 | m = len(pareto_front) 120 | try: 121 | pi_x = norm.cdf((pareto_front[0][0] - moy1) / sig1) 122 | for i in range(1, m - 1): 123 | pi_x += ( 124 | norm.cdf((pareto_front[i + 1][0] - moy1) / sig1) 125 | - norm.cdf((pareto_front[i][0] - moy1) / sig1) 126 | ) * norm.cdf((pareto_front[i + 1][1] - moy2) / sig2) 127 | pi_x += (1 - norm.cdf((pareto_front[m - 1][0] - moy1) / sig1)) * norm.cdf( 128 | (pareto_front[m - 1][1] - moy2) / sig2 129 | ) 130 | return pi_x 131 | except ZeroDivisionError: # for training points -> variances = 0 132 | return 0 133 | 134 | @staticmethod 135 | def psi(a, b, µ, s): 136 | return s * norm.pdf((b - µ) / s) + (a - µ) * norm.cdf((b - µ) / s) 137 | 138 | def EHVI( 139 | self, 140 | x, 141 | ): 142 | """ 143 | Expected hypervolume improvement of the point x for 2 objectives. 144 | If more than 2 objectives, computed using Monte-Carlo sampling instead 145 | 146 | Parameters 147 | ---------- 148 | x : list 149 | coordinates in the design space of the point to evaluate. 150 | 151 | Returns 152 | ------- 153 | float 154 | Expected HVImprovement 155 | """ 156 | 157 | x = np.asarray(x).reshape(1, -1) 158 | f = Criterion._compute_pareto(self.models) 159 | 160 | if len(self.models) > 2: 161 | y = np.asarray( 162 | [ 163 | mod.predict_values(x)[0][0] 164 | - 3 * mod.predict_variances(x)[0][0] ** 0.5 165 | for mod in self.models 166 | ] 167 | ) 168 | if Criterion.is_dominated(y, f): 169 | return 0 # the point - 3sigma is dominated, no chances to improve hv 170 | MC = MonteCarlo(random_state=self.random_state) 171 | q = MC.sampling(x, self.models, self.points) 172 | return ( 173 | sum([self.hv(np.vstack((f, qi))) for qi in q]) / self.points 174 | ) # maybe we can remove the division by self.points as there is the same amount of points for each call? It's just for scale here 175 | 176 | variances = [mod.predict_variances for mod in self.models] 177 | s1, s2 = variances[0](x)[0][0] ** 0.5, variances[1](x)[0][0] ** 0.5 178 | if s1 == 0 or s2 == 0: # training point 179 | return 0 180 | moyennes = [mod.predict_values for mod in self.models] 181 | µ1, µ2 = moyennes[0](x)[0][0], moyennes[1](x)[0][0] 182 | f.sort(key=lambda x: x[0]) 183 | f.insert(0, np.array([self.ref[0], -1e15])) # 1e15 to approximate infinity 184 | f.append(np.array([-1e15, self.ref[1]])) 185 | res1, res2 = 0, 0 186 | for i in range(len(f) - 1): 187 | res1 += ( 188 | (f[i][0] - f[i + 1][0]) 189 | * norm.cdf((f[i + 1][0] - µ1) / s1) 190 | * Criterion.psi(f[i + 1][1], f[i + 1][1], µ2, s2) 191 | ) 192 | res2 += ( 193 | Criterion.psi(f[i][0], f[i][0], µ1, s1) 194 | - Criterion.psi(f[i][0], f[i + 1][0], µ1, s1) 195 | ) * Criterion.psi(f[i][1], f[i][1], µ2, s2) 196 | return res1 + res2 197 | 198 | def HV(self, x): 199 | """ 200 | hypervolume if x is the new point added. Only the mean is taken, 201 | so it doesn't explore as it doesn't take in account the incertitude. 202 | A good idea is to combine it with the var. (to do : look if ucb is doing this) 203 | 204 | Parameters 205 | ---------- 206 | x : list 207 | coordinates in the design space of the point to evaluate. 208 | 209 | Returns 210 | ------- 211 | out : float 212 | Hypervolume of the current front concatened with µ(x) 213 | """ 214 | x = np.asarray(x).reshape(1, -1) 215 | pf = Criterion._compute_pareto(self.models) 216 | moyennes = [mod.predict_values for mod in self.models] 217 | y = np.asarray([moy(x)[0][0] for moy in moyennes]) 218 | return self.hv(np.vstack((pf, y))) 219 | 220 | def WB2S(self, x): 221 | """ 222 | Criterion WB2S multi-objective adapted from the paper "Adapated 223 | modeling strategy for constrained optimization with application 224 | to aerodynamic wing design" : 225 | WB2S(x) = s*subcriterion(x) - transformation( µ(x) ) 226 | 227 | Parameters 228 | ---------- 229 | x : list 230 | coordinates in the design space of the point to evaluate. 231 | 232 | Returns 233 | ------- 234 | WBS2 : float 235 | """ 236 | x = np.asarray(x).reshape(1, -1) 237 | moyennes = [mod.predict_values for mod in self.models] 238 | µ = [moy(x)[0][0] for moy in moyennes] 239 | return self.s * self.subcrit(x) - self.transfo(µ) 240 | 241 | @staticmethod 242 | def _compute_pareto(modeles): 243 | """ 244 | Set curr_pareto_front to the non-dominated training points. 245 | It allows to compute it once for a complete enrichment step 246 | """ 247 | ydata = np.transpose( 248 | np.asarray([mod.training_points[None][0][1] for mod in modeles]) 249 | )[0] 250 | pareto_index = Criterion.pareto(ydata) 251 | # self.curr_pareto_front = [ydata[i] for i in pareto_index] 252 | # I remove this and the associated self. variable beacause for a reason that I do not unserstand, it is way faster to recompute it at every call than to store it 253 | return [ydata[i] for i in pareto_index] 254 | 255 | @staticmethod 256 | def pareto(Y): 257 | """ 258 | Parameters 259 | ---------- 260 | Y : list of arrays 261 | list of the points to compare. 262 | 263 | Returns 264 | ------- 265 | index : list 266 | list of the indexes in Y of the Pareto-optimal points. 267 | """ 268 | index = [] # indexes of the best points (Pareto) 269 | n = len(Y) 270 | dominated = [False] * n 271 | for y in range(n): 272 | if not dominated[y]: 273 | for y2 in range(y + 1, n): 274 | if not dominated[ 275 | y2 276 | ]: # if y2 is dominated (by y0), we already compared y0 to y 277 | y_domine_y2, y2_domine_y = Criterion.dominate_min(Y[y], Y[y2]) 278 | 279 | if y_domine_y2: 280 | dominated[y2] = True 281 | if y2_domine_y: 282 | dominated[y] = True 283 | break 284 | if not dominated[y]: 285 | index.append(y) 286 | return index 287 | 288 | # returns a-dominates-b , b-dominates-a !! for minimization !! 289 | @staticmethod 290 | def dominate_min(a, b): 291 | """ 292 | Parameters 293 | ---------- 294 | a : array or list 295 | coordinates in the objective space. 296 | b : array or list 297 | same thing than a. 298 | 299 | Returns 300 | ------- 301 | bool 302 | a dominates b (in terms of minimization !). 303 | bool 304 | b dominates a (in terms of minimization !). 305 | """ 306 | a_bat_b = False 307 | b_bat_a = False 308 | for i in range(len(a)): 309 | if a[i] < b[i]: 310 | a_bat_b = True 311 | if b_bat_a: 312 | return False, False # same front 313 | if a[i] > b[i]: 314 | b_bat_a = True 315 | if a_bat_b: 316 | return False, False 317 | if a_bat_b and (not b_bat_a): 318 | return True, False 319 | if b_bat_a and (not a_bat_b): 320 | return False, True 321 | return False, False # same values 322 | 323 | @staticmethod 324 | def is_dominated(y, pf): 325 | """True if y is dominated by a point of pf""" 326 | for z in pf: 327 | battu, _ = Criterion.dominate_min(z, y) 328 | if battu: 329 | return True 330 | return False 331 | 332 | @staticmethod 333 | def prob_of_feasability(x, const_modeles): 334 | """ 335 | Product of the probabilities that x is a feasible solution, 336 | assuming that the constraints are independents, and modelized by 337 | gaussian models. 338 | """ 339 | means = [mod.predict_values for mod in const_modeles] 340 | var = [mod.predict_variances for mod in const_modeles] 341 | x = np.asarray(x).reshape(1, -1) 342 | probs = [ 343 | norm.cdf(-means[i](x)[0][0] / var[i](x)[0][0]) 344 | for i in range(len(const_modeles)) 345 | ] 346 | return np.prod(probs) 347 | -------------------------------------------------------------------------------- /smoot/smoot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Mar 31 14:08:54 2021 4 | 5 | @author: robin grapin 6 | """ 7 | 8 | import numpy as np 9 | 10 | from pymoo.algorithms.moo.nsga2 import NSGA2 11 | from pymoo.core.problem import ElementwiseProblem 12 | from pymoo.optimize import minimize 13 | from pymoo.indicators.hv import HV 14 | 15 | from smt.applications.application import SurrogateBasedApplication 16 | from smt.surrogate_models import KRG, KPLS 17 | from smt.sampling_methods import LHS 18 | 19 | from smoot.criterion import Criterion 20 | 21 | 22 | class MOO(SurrogateBasedApplication): 23 | def _initialize(self): 24 | 25 | super()._initialize() 26 | declare = self.options.declare 27 | 28 | declare( 29 | "surrogate", 30 | "KPLS", 31 | types=str, 32 | values=["KRG", "KPLS"], 33 | desc="Surrogate model type", 34 | ) 35 | declare( 36 | "const", 37 | [], 38 | types=list, 39 | desc="constraints of the problem, should be <=0 constraints, taking x = ndarray[ne,nx], out ndarray[ne,1] each", 40 | ) 41 | declare( 42 | "penal", 43 | True, 44 | types=bool, 45 | desc="True to maximize the criterion with penalty, False for subject to constraint's models", 46 | ) 47 | declare( 48 | "subcrit", 49 | "EHVI", 50 | types=str, 51 | values=["PI", "MPI", "EHVI"], 52 | desc="subcriterion for the formula of wb2s : s*subcrit - transfo(µ)", 53 | ) 54 | declare( 55 | "transfo", 56 | lambda l: sum(l), 57 | types=type(lambda x: x), 58 | desc="transfo function for wb2s formula : s*subcrit - transfo(µ)", 59 | ) 60 | declare( 61 | "criterion", 62 | "PI", 63 | types=str, 64 | values=["PI", "EHVI", "GA", "WB2S", "MPI"], 65 | desc="infill criterion", 66 | ) 67 | declare("n_iter", 10, types=int, desc="Number of optimizer steps") 68 | declare("xlimits", None, types=np.ndarray, desc="Bounds of function fun inputs") 69 | declare("n_start", 20, types=int, desc="Number of optimization start points") 70 | declare( 71 | "pop_size", 72 | 50, 73 | types=int, 74 | desc="number of individuals for the genetic algorithm", 75 | ) 76 | declare( 77 | "n_gen", 78 | 50, 79 | types=int, 80 | desc="number generations for the genetic algorithm", 81 | ) 82 | declare( 83 | "q", 84 | 0.5, 85 | types=float, 86 | desc="importance ratio of design space in comparation to objective space when chosing a point with GA", 87 | ) 88 | 89 | declare("verbose", False, types=bool, desc="Print computation information") 90 | declare( 91 | "xdoe", 92 | None, 93 | types=np.ndarray, 94 | desc="Initial doe inputs. DoE formats are ndarray[n_start,n_dim]", 95 | ) 96 | declare("ydoe", None, types=np.ndarray, desc="Initial doe outputs") 97 | declare( 98 | "ydoe_c", None, types=np.ndarray, desc="initial doe outputs for constraints" 99 | ) 100 | declare( 101 | "random_state", 102 | None, 103 | types=(type(None), int), 104 | desc="seed number which controls random draws", 105 | ) 106 | 107 | def optimize(self, fun): 108 | """ 109 | Optimize the multi-objective function fun. At the end, the object's item 110 | .modeles is a SMT surrogate_model object with the most precise fun's model 111 | .result is the result of its optimization thanks to NSGA2 112 | 113 | Parameters 114 | ---------- 115 | fun : function 116 | function taking x=ndarray[ne,ndim], 117 | returning y = ndarray[ne,ny] 118 | where y[i][j] = fj(xi). 119 | If fun has only one objective, y = ndarray[ne, 1] 120 | 121 | Returns 122 | ------- 123 | self.result.X : ndarray[int,n_var] 124 | Pareto Set. 125 | self.result.F : ndarray[int,ny] 126 | Pareto Front. 127 | """ 128 | if type(self.options["xlimits"]) != np.ndarray: 129 | try: 130 | self.options["xlimits"] = fun.xlimits 131 | except AttributeError: # if fun doesn't have "xlimits" attribute 132 | raise AttributeError("Error : No bounds given") 133 | return 134 | 135 | self.seed = np.random.RandomState(self.options["random_state"]) 136 | self.n_const = len(self.options["const"]) 137 | x_data, y_data, y_data_c = self._setup_optimizer(fun) 138 | self.ndim = self.options["xlimits"].shape[0] 139 | self.ny = y_data.shape[-1] 140 | 141 | if self.ny == 1: 142 | self.log("EGO will be used as there is only 1 objective") 143 | if self.n_const > 0: 144 | self.log("EGO doesn't take constraints in account") 145 | self.use_ego(fun, x_data, y_data) 146 | self.log( 147 | "Optimization done, get the front with .result.F and the set with .result.X" 148 | ) 149 | return 150 | 151 | # obtaining models for each objective 152 | self.modelize(x_data, y_data, y_data_c) 153 | 154 | if type(y_data) != list: 155 | y_data = list(y_data) 156 | 157 | for k in range(self.options["n_iter"]): 158 | 159 | self.log(str("iteration " + str(k + 1))) 160 | 161 | # find next best x-coord point to evaluate 162 | new_x, _ = self._find_best_point(self.options["criterion"]) 163 | new_x = np.array([new_x]) 164 | new_y = fun(new_x) 165 | 166 | # update model with the new point 167 | y_data = np.atleast_2d(np.append(y_data, new_y, axis=0)) 168 | x_data = np.atleast_2d(np.append(x_data, new_x, axis=0)) 169 | 170 | # update the constraints 171 | if self.n_const > 0: 172 | new_y_c = np.atleast_2d( 173 | np.array( 174 | [ 175 | self.options["const"][i](new_x)[0] 176 | for i in range(self.n_const) 177 | ] 178 | ) 179 | ) 180 | y_data_c = np.atleast_2d(np.append(y_data_c, new_y_c, axis=0)) 181 | 182 | self.modelize(x_data, y_data, y_data_c) 183 | 184 | self.log("Model is well refined, NSGA2 is running...") 185 | self.result = minimize( 186 | self.def_prob( 187 | n_var=self.ndim, 188 | xbounds=self.options["xlimits"], 189 | n_obj=self.ny, 190 | obj=self.modeles, 191 | n_const=self.n_const, 192 | const=self.const_modeles, 193 | ), 194 | NSGA2( 195 | pop_size=2 * self.options["pop_size"], seed=self.options["random_state"] 196 | ), 197 | ("n_gen", 2 * self.options["n_gen"]), 198 | seed=self.options["random_state"], 199 | ) 200 | self.log( 201 | "Optimization done, get the front with .result.F and the set with .result.X" 202 | ) 203 | return self.result.X, self.result.F 204 | 205 | def _setup_optimizer(self, fun): 206 | """ 207 | Parameters 208 | ---------- 209 | fun : objective function 210 | 211 | Returns 212 | ------- 213 | xt : array of arrays 214 | sampling points in the design space. 215 | yt : list of arrays 216 | yt[i] = fi(xt). 217 | 218 | """ 219 | xt, yt, yc = self.options["xdoe"], self.options["ydoe"], self.options["ydoe_c"] 220 | if xt is None and not (yt is None and yc is None): 221 | print("xdoe must be an array if you want to use ydoe or ydoe_c") 222 | yt, yc = None, None 223 | if xt is None: 224 | sampling = LHS( 225 | xlimits=self.options["xlimits"], 226 | random_state=self.options["random_state"], 227 | ) 228 | xt = sampling(self.options["n_start"]) 229 | if yt is None: 230 | yt = fun(xt) 231 | if yc is None and self.n_const > 0: 232 | yc = np.array([np.array(con(xt)) for con in self.options["const"]]).T #!!! 233 | return xt, yt, yc 234 | 235 | def modelize(self, xt, yt, yt_const=None): 236 | """ 237 | Creates and train a krige model with the given datapoints 238 | 239 | Parameters 240 | ---------- 241 | xt : ndarray[n_points, n_dim] 242 | Design space coordinates of the training points. 243 | yt : ndarray[n_points, n_objectives] 244 | Training outputs. 245 | yt_const : list of ndarray[nt,ny] 246 | constraints training outputs 247 | """ 248 | self.modeles = [] 249 | for iny in range(self.ny): 250 | t = ( 251 | KRG(print_global=False) 252 | if self.options["surrogate"] == "KRG" 253 | else KPLS(print_global=False) 254 | ) 255 | t.set_training_values(xt, yt[:, iny]) 256 | t.train() 257 | self.modeles.append(t) 258 | 259 | self.const_modeles = [] 260 | if not (yt_const is None): 261 | for iny in range(self.n_const): 262 | t = ( 263 | KRG(print_global=False) 264 | if self.options["surrogate"] == "KRG" 265 | else KPLS(print_global=False) 266 | ) 267 | t.set_training_values(xt, yt_const[:, iny]) 268 | t.train() 269 | self.const_modeles.append(t) 270 | 271 | def def_prob(self, n_var, xbounds, n_obj, obj, n_const, const): 272 | """ 273 | Creates the pymoo Problem object with the surrogate as objective 274 | 275 | Returns 276 | ------- 277 | MyProblem : pymoo.problem 278 | """ 279 | 280 | class MyProblem(ElementwiseProblem): 281 | def __init__(self): 282 | super().__init__( 283 | n_var=n_var, 284 | n_obj=n_obj, 285 | n_constr=n_const, 286 | xl=np.asarray([i[0] for i in xbounds]), 287 | xu=np.asarray([i[1] for i in xbounds]), 288 | ) 289 | 290 | def _evaluate(self, x, out, *args, **kwargs): 291 | if n_obj > 1: 292 | xx = np.asarray(x).reshape(1, -1) 293 | out["F"] = [f.predict_values(xx)[0][0] for f in obj] 294 | if n_const > 0: 295 | out["G"] = [g.predict_values(xx)[0][0] for g in const] 296 | 297 | else: # 1 obj is for acquisition function 298 | out["F"] = obj(x) 299 | if n_const > 0: # case without penalization 300 | xx = np.asarray(x).reshape(1, -1) 301 | out["G"] = [g.predict_values(xx)[0][0] for g in const] 302 | 303 | return MyProblem() 304 | 305 | def _find_best_point(self, criter): 306 | """ 307 | Selects the best point to refine the model according to 308 | the chosen infill criterion. 309 | 310 | Returns 311 | ------- 312 | ndarray 313 | next point for the model update. 314 | """ 315 | if criter == "GA": 316 | res = minimize( 317 | self.def_prob( 318 | n_var=self.ndim, 319 | xbounds=self.options["xlimits"], 320 | n_obj=self.ny, 321 | obj=self.modeles, 322 | n_const=self.n_const, 323 | const=self.const_modeles, 324 | ), 325 | NSGA2( 326 | pop_size=self.options["pop_size"], seed=self.options["random_state"] 327 | ), 328 | ("n_gen", self.options["n_gen"]), 329 | ) 330 | X = res.X 331 | Y = res.F 332 | ydata = np.transpose( 333 | np.asarray([mod.training_points[None][0][1] for mod in self.modeles]) 334 | )[0] 335 | xdata = self.modeles[0].training_points[None][0][0] 336 | # MOBOpt criterion 337 | q = self.options["q"] 338 | n = ydata.shape[1] 339 | d_l_x = [sum([np.linalg.norm(xj - xi) for xj in xdata]) / n for xi in X] 340 | d_l_f = [sum([np.linalg.norm(yj - yi) for yj in ydata]) / n for yi in Y] 341 | µ_x = np.mean(d_l_x) 342 | µ_f = np.mean(d_l_f) 343 | var_x, var_f = np.var(d_l_x), np.var(d_l_f) 344 | if var_x == 0 or var_f == 0: 345 | return X[self.seed.randint(len(X)), :] 346 | dispersion = [ 347 | q * (d_l_x[j] - µ_x) / var_x + (1 - q) * (d_l_f[j] - µ_f) / var_f 348 | for j in range(X.shape[0]) 349 | ] 350 | i = dispersion.index(max(dispersion)) 351 | return X[i, :] 352 | 353 | if criter == "PI": 354 | PI = Criterion( 355 | "PI", 356 | self.modeles, 357 | random_state=self.options["random_state"], 358 | ) 359 | self.obj_k = lambda x: -PI(x) 360 | 361 | if criter == "MPI": 362 | MPI = Criterion( 363 | "MPI", self.modeles, random_state=self.options["random_state"] 364 | ) 365 | self.obj_k = lambda x: -MPI(x) 366 | 367 | if criter == "EHVI": 368 | ydata = np.transpose( 369 | np.asarray([mod.training_points[None][0][1] for mod in self.modeles]) 370 | )[0] 371 | ref = [ydata[:, i].max() + 1 for i in range(self.ny)] # nadir +1 372 | hv = HV(ref_point=np.asarray(ref)) 373 | EHVI = Criterion( 374 | "EHVI", 375 | self.modeles, 376 | ref=ref, 377 | hv=hv, 378 | random_state=self.options["random_state"], 379 | ) 380 | self.obj_k = lambda x: -EHVI(x) 381 | 382 | if criter == "WB2S": 383 | xmax, valmax = self._find_best_point(self.options["subcrit"]) 384 | if valmax == 0: 385 | s = 1 386 | else: 387 | moyennes = [mod.predict_values for mod in self.modeles] 388 | beta = 100 # hyperparameter 389 | s = ( 390 | beta 391 | * self.options["transfo"]( 392 | [moy(np.asarray(xmax).reshape(1, -1))[0][0] for moy in moyennes] 393 | ) 394 | / valmax 395 | ) 396 | if self.options["subcrit"] == "EHVI": 397 | ydata = np.transpose( 398 | np.asarray( 399 | [mod.training_points[None][0][1] for mod in self.modeles] 400 | ) 401 | )[0] 402 | ref = [ydata[:, i].max() + 1 for i in range(self.ny)] # nadir +1 403 | hv = HV(ref_point=np.asarray(ref)) 404 | else: 405 | ref, hv = None, None 406 | subcriterion = Criterion( 407 | self.options["subcrit"], 408 | self.modeles, 409 | hv=hv, 410 | ref=ref, 411 | random_state=self.options["random_state"], 412 | ) 413 | WB2S = Criterion( 414 | "WB2S", 415 | self.modeles, 416 | s=s, 417 | random_state=self.options["random_state"], 418 | subcrit=subcriterion, 419 | transfo=self.options["transfo"], 420 | ) 421 | self.obj_k = lambda x: -WB2S(x) 422 | 423 | if self.options["penal"] and self.n_const > 0: 424 | prob = self.def_prob( 425 | n_var=self.ndim, 426 | xbounds=self.options["xlimits"], 427 | n_obj=1, 428 | obj=self.penal(self.obj_k), 429 | n_const=0, 430 | const=[], 431 | ) 432 | 433 | else: 434 | prob = self.def_prob( 435 | n_var=self.ndim, 436 | xbounds=self.options["xlimits"], 437 | n_obj=1, 438 | obj=self.obj_k, 439 | n_const=self.n_const, 440 | const=self.const_modeles, 441 | ) 442 | 443 | maximizers = minimize( 444 | prob, 445 | NSGA2(pop_size=self.options["pop_size"], seed=self.options["random_state"]), 446 | ("n_gen", self.options["n_gen"]), 447 | seed=self.options["random_state"], 448 | ).X 449 | x_opt = ( 450 | maximizers 451 | if len(maximizers.shape) == 1 452 | else maximizers[self.seed.randint(len(maximizers))] 453 | ) 454 | self.log(criter + " max value : " + str(-self.obj_k(x_opt))) 455 | self.log("xopt : " + str(x_opt)) 456 | for i in range(self.n_const): 457 | self.log( 458 | "constraint " 459 | + str(i) 460 | + " estimated value : " 461 | + str(self.const_modeles[i].predict_values(np.array([x_opt]))[0][0]) 462 | ) 463 | return x_opt, -self.obj_k(x_opt) 464 | 465 | def penal(self, f): 466 | """ 467 | "Penalized through weightening" criterion by the probability 468 | of feasability at each point 469 | 470 | Parameters 471 | ---------- 472 | f : function 473 | Criterion to minimize (because f : x -> - criterion(x) ). 474 | 475 | Returns 476 | ------- 477 | function 478 | weighted function. 479 | """ 480 | return lambda x: (f(x) - 0.01) * Criterion.prob_of_feasability( 481 | x, self.const_modeles 482 | ) 483 | # 0.01 because the criterion is often equal to 0, so it favorises the reachable points 484 | 485 | def log(self, msg): 486 | if self.options["verbose"]: 487 | print(msg) 488 | 489 | def use_ego(self, fun, xdoe, ydoe): 490 | """ 491 | Call ego to find the optimum of the 1D-valued funcion fun. 492 | The set and front are stored, as usual, in the pymoo.model.algorithm 493 | class result in .result.X and .result.F 494 | 495 | Parameters 496 | ---------- 497 | fun : function 498 | function with one output. 499 | 500 | """ 501 | from smt.applications import EGO 502 | from pymoo.core.algorithm import Algorithm 503 | 504 | ego = EGO( 505 | xdoe=xdoe, 506 | ydoe=ydoe, 507 | n_iter=self.options["n_iter"], 508 | criterion="EI", 509 | n_start=self.options["n_start"], 510 | xlimits=self.options["xlimits"], 511 | verbose=self.options["verbose"], 512 | random_state=self.options["random_state"], 513 | ) 514 | x_opt, y_opt, _, _, _ = ego.optimize(fun) 515 | self.result = Algorithm() 516 | self.result.X = np.array([[x_opt]]) 517 | self.result.F = np.array([[y_opt]]) 518 | self.log( 519 | "Optimization done, get the front with .result.F and the set with .result.X" 520 | ) 521 | return x_opt, y_opt 522 | --------------------------------------------------------------------------------