├── 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 | [](https://github.com/OneraHub/smoot/actions?query=workflow%3ATests)
2 | [](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 | 
21 | 
22 |
23 | 
24 |
25 | 
26 | 
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 |
--------------------------------------------------------------------------------