├── sb_arch_opt
├── algo
│ ├── __init__.py
│ ├── arch_sbo
│ │ ├── __init__.py
│ │ ├── metrics.py
│ │ └── api.py
│ ├── egor_interface
│ │ ├── __init__.py
│ │ └── api.py
│ ├── hebo_interface
│ │ ├── __init__.py
│ │ ├── api.py
│ │ └── algo.py
│ ├── pymoo_interface
│ │ ├── __init__.py
│ │ ├── random_search.py
│ │ ├── md_mating.py
│ │ ├── api.py
│ │ └── metrics.py
│ ├── smarty_interface
│ │ ├── __init__.py
│ │ ├── api.py
│ │ └── algo.py
│ ├── tpe_interface
│ │ ├── __init__.py
│ │ └── api.py
│ ├── botorch_interface
│ │ ├── __init__.py
│ │ ├── api.py
│ │ └── algo.py
│ ├── segomoe_interface
│ │ ├── __init__.py
│ │ ├── api.py
│ │ └── pymoo_algo.py
│ └── trieste_interface
│ │ ├── __init__.py
│ │ └── api.py
├── problems
│ ├── __init__.py
│ ├── continuous.py
│ ├── md_mo.py
│ └── constrained.py
├── tests
│ ├── __init__.py
│ ├── algo
│ │ ├── __init__.py
│ │ ├── test_smarty.py
│ │ ├── test_botorch.py
│ │ ├── test_hebo.py
│ │ ├── test_tpe.py
│ │ ├── test_egor.py
│ │ ├── test_trieste.py
│ │ └── test_segomoe.py
│ ├── problems
│ │ ├── __init__.py
│ │ ├── test_discrete.py
│ │ ├── test_continuous.py
│ │ ├── test_constrained.py
│ │ ├── test_assignment.py
│ │ ├── test_hidden_constraints.py
│ │ ├── test_gnc.py
│ │ ├── test_md_mo.py
│ │ ├── test_rocket.py
│ │ ├── test_hierarchical.py
│ │ └── test_turbofan_arch.py
│ ├── test_tutorials.py
│ ├── conftest.py
│ ├── test_design_space.py
│ └── test_correction.py
├── __init__.py
└── util.py
├── requirements-tests.txt
├── docs
├── style.css
├── icon.png
├── overrides
│ └── partials
│ │ └── copyright.html
├── api
│ ├── hebo.md
│ ├── smarty.md
│ ├── segomoe.md
│ ├── botorch.md
│ ├── egor.md
│ ├── trieste.md
│ ├── arch_sbo.md
│ ├── pymoo.md
│ └── problem.md
└── algo
│ ├── smarty.md
│ ├── hebo.md
│ ├── botorch.md
│ ├── tpe.md
│ ├── trieste.md
│ ├── arch_sbo.md
│ ├── egor.md
│ ├── segomoe.md
│ └── pymoo.md
├── requirements-docs.txt
├── requirements-assignment.txt
├── pyproject.toml
├── requirements-ota.txt
├── SBArchOpt DLR Individual Contributor License Agreement.docx
├── .github
├── dependabot.yml
└── workflows
│ ├── publish.yml
│ ├── tests.yml
│ ├── tests_basic.yml
│ └── tests_slow.yml
├── binder
└── environment.yml
├── .readthedocs.yaml
├── CITATION.cff
├── LICENSE
├── mkdocs.yml
├── setup.py
├── README.md
└── .gitignore
/sb_arch_opt/algo/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sb_arch_opt/problems/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/algo/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sb_arch_opt/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '1.5.7'
2 |
--------------------------------------------------------------------------------
/requirements-tests.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | testbook~=0.4.2
3 | matplotlib
--------------------------------------------------------------------------------
/sb_arch_opt/algo/arch_sbo/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import *
2 |
--------------------------------------------------------------------------------
/docs/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --md-primary-fg-color: #330a5e;
3 | }
--------------------------------------------------------------------------------
/sb_arch_opt/algo/egor_interface/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import *
2 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/hebo_interface/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import *
2 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/pymoo_interface/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import *
2 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/smarty_interface/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import *
2 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/tpe_interface/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import *
2 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/botorch_interface/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import *
2 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/segomoe_interface/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import *
2 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/trieste_interface/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import *
2 |
--------------------------------------------------------------------------------
/docs/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbussemaker/SBArchOpt/HEAD/docs/icon.png
--------------------------------------------------------------------------------
/requirements-docs.txt:
--------------------------------------------------------------------------------
1 | mkdocs
2 | mkdocstrings[python]
3 | mkdocs-jupyter
4 | mkdocs-material
--------------------------------------------------------------------------------
/requirements-assignment.txt:
--------------------------------------------------------------------------------
1 | git+https://github.com/jbussemaker/AssignmentEncoding#egg=assign_enc
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools"]
3 | build-backend = "setuptools.build_meta"
4 |
--------------------------------------------------------------------------------
/requirements-ota.txt:
--------------------------------------------------------------------------------
1 | git+https://github.com/jbussemaker/OpenTurbofanArchitecting@pymoo_optional#egg=open_turb_arch
2 | scikit-learn
3 | joblib
--------------------------------------------------------------------------------
/SBArchOpt DLR Individual Contributor License Agreement.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbussemaker/SBArchOpt/HEAD/SBArchOpt DLR Individual Contributor License Agreement.docx
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # Maintain dependencies for GitHub Actions
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly"
--------------------------------------------------------------------------------
/binder/environment.yml:
--------------------------------------------------------------------------------
1 | name: sbarchopt
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - python=3.9
6 | - numpy
7 | - scipy
8 | - pip
9 | - pip:
10 | - sb-arch-opt[arch_sbo]
11 | - matplotlib
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: ubuntu-22.04
5 | tools:
6 | python: "3.10"
7 |
8 | mkdocs:
9 | configuration: mkdocs.yml
10 |
11 | python:
12 | install:
13 | - requirements: requirements-docs.txt
14 |
--------------------------------------------------------------------------------
/docs/overrides/partials/copyright.html:
--------------------------------------------------------------------------------
1 |
2 |

3 | {% if config.copyright %}
4 |
5 | {{ config.copyright }}
6 |
7 | {% endif %}
8 |
9 |
--------------------------------------------------------------------------------
/docs/api/hebo.md:
--------------------------------------------------------------------------------
1 | # HEBO API Reference
2 |
3 | [Installation and usage](../algo/hebo.md)
4 |
5 | ::: sb_arch_opt.algo.hebo_interface.get_hebo_optimizer
6 | handler: python
7 |
8 | ::: sb_arch_opt.algo.hebo_interface.algo.HEBOArchOptInterface
9 | handler: python
10 | options:
11 | members:
12 | - optimize
13 | - ask
14 | - tell
15 | - pop
16 |
--------------------------------------------------------------------------------
/docs/api/smarty.md:
--------------------------------------------------------------------------------
1 | # SMARTy API Reference
2 |
3 | [Installation and usage](../algo/smarty.md)
4 |
5 | ::: sb_arch_opt.algo.smarty_interface.get_smarty_optimizer
6 | handler: python
7 |
8 | ::: sb_arch_opt.algo.smarty_interface.algo.SMARTyArchOptInterface
9 | handler: python
10 | options:
11 | members:
12 | - optimize
13 | - optimizer
14 | - pop
15 |
--------------------------------------------------------------------------------
/docs/api/segomoe.md:
--------------------------------------------------------------------------------
1 | # SEGOMOE API Reference
2 |
3 | [Installation and usage](../algo/segomoe.md)
4 |
5 | ::: sb_arch_opt.algo.segomoe_interface.SEGOMOEInterface
6 | handler: python
7 | options:
8 | members:
9 | - initialize_from_previous
10 | - run_optimization
11 | - run_doe
12 | - x
13 | - f
14 | - g
15 | - pop
16 | - opt
17 |
--------------------------------------------------------------------------------
/docs/api/botorch.md:
--------------------------------------------------------------------------------
1 | # BoTorch (Ax) API Reference
2 |
3 | [Installation and usage](../algo/botorch.md)
4 |
5 | ::: sb_arch_opt.algo.botorch_interface.api.get_botorch_interface
6 | handler: python
7 |
8 | ::: sb_arch_opt.algo.botorch_interface.algo.AxInterface
9 | handler: python
10 | options:
11 | members:
12 | - get_optimization_loop
13 | - get_search_space
14 | - get_population
15 |
--------------------------------------------------------------------------------
/docs/api/egor.md:
--------------------------------------------------------------------------------
1 | # Egor API Reference
2 |
3 | [Installation and usage](../algo/egor.md)
4 |
5 | ::: sb_arch_opt.algo.egor_interface.algo.EgorArchOptInterface
6 | handler: python
7 | options:
8 | members:
9 | - initialize_from_previous
10 | - minimize
11 | - egor
12 | - design_space
13 | - pop
14 | - x
15 | - f
16 | - g
17 |
18 |
--------------------------------------------------------------------------------
/docs/api/trieste.md:
--------------------------------------------------------------------------------
1 | # Trieste API Reference
2 |
3 | [Installation and usage](../algo/trieste.md)
4 |
5 | ::: sb_arch_opt.algo.trieste_interface.api.get_trieste_optimizer
6 | handler: python
7 |
8 | ::: sb_arch_opt.algo.trieste_interface.algo.ArchOptBayesianOptimizer
9 | handler: python
10 | options:
11 | members:
12 | - initialize_from_previous
13 | - run_optimization
14 | - to_population
15 |
--------------------------------------------------------------------------------
/docs/api/arch_sbo.md:
--------------------------------------------------------------------------------
1 | # ArchSBO API Reference
2 |
3 | [Installation and usage](../algo/arch_sbo.md)
4 |
5 | ::: sb_arch_opt.algo.arch_sbo.api.get_arch_sbo_gp
6 | handler: python
7 |
8 | ::: sb_arch_opt.algo.arch_sbo.api.get_arch_sbo_rbf
9 | handler: python
10 |
11 | ::: sb_arch_opt.algo.arch_sbo.algo.InfillAlgorithm
12 | handler: python
13 | options:
14 | members:
15 | - store_intermediate_results
16 | - initialize_from_previous_results
17 |
18 | ::: sb_arch_opt.algo.arch_sbo.api.get_sbo
19 | handler: python
20 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/test_discrete.py:
--------------------------------------------------------------------------------
1 | from sb_arch_opt.problems.discrete import *
2 | from sb_arch_opt.tests.problems.test_md_mo import run_test_no_hierarchy
3 |
4 |
5 | def test_md_branin():
6 | run_test_no_hierarchy(MDBranin())
7 |
8 |
9 | def test_aug_md_branin():
10 | run_test_no_hierarchy(AugmentedMDBranin())
11 |
12 |
13 | def test_md_goldstein():
14 | run_test_no_hierarchy(MDGoldstein())
15 |
16 |
17 | def test_munoz_zuniga():
18 | run_test_no_hierarchy(MunozZunigaToy())
19 |
20 |
21 | def test_halstrup4():
22 | run_test_no_hierarchy(Halstrup04())
23 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/test_continuous.py:
--------------------------------------------------------------------------------
1 | from sb_arch_opt.problems.continuous import *
2 | from sb_arch_opt.tests.problems.test_md_mo import run_test_no_hierarchy
3 |
4 |
5 | def test_himmelblau():
6 | run_test_no_hierarchy(Himmelblau())
7 | Himmelblau().get_discrete_rates(show=True)
8 |
9 |
10 | def test_rosenbrock():
11 | run_test_no_hierarchy(Rosenbrock())
12 |
13 |
14 | def test_griewank():
15 | run_test_no_hierarchy(Griewank())
16 |
17 |
18 | def test_goldstein():
19 | run_test_no_hierarchy(Goldstein())
20 |
21 |
22 | def test_branin():
23 | run_test_no_hierarchy(Branin())
24 |
--------------------------------------------------------------------------------
/docs/api/pymoo.md:
--------------------------------------------------------------------------------
1 | # pymoo Interface API Reference
2 |
3 | [Installation and usage](../algo/pymoo.md)
4 |
5 | ::: sb_arch_opt.algo.pymoo_interface.api.get_nsga2
6 | handler: python
7 |
8 | ::: sb_arch_opt.algo.pymoo_interface.api.get_doe_algo
9 | handler: python
10 |
11 | ::: sb_arch_opt.algo.pymoo_interface.api.initialize_from_previous_results
12 | handler: python
13 |
14 | ::: sb_arch_opt.algo.pymoo_interface.api.load_from_previous_results
15 | handler: python
16 |
17 | ::: sb_arch_opt.algo.pymoo_interface.api.provision_pymoo
18 | handler: python
19 |
20 | ::: sb_arch_opt.algo.pymoo_interface.api.ArchOptNSGA2
21 | handler: python
22 |
23 | ::: sb_arch_opt.algo.pymoo_interface.api.DOEAlgorithm
24 | handler: python
25 |
--------------------------------------------------------------------------------
/CITATION.cff:
--------------------------------------------------------------------------------
1 | cff-version: "1.2.0"
2 | authors:
3 | - family-names: Bussemaker
4 | given-names: Jasper H.
5 | orcid: "https://orcid.org/0000-0002-5421-6419"
6 | doi: 10.5281/zenodo.8318765
7 | message: If you use this software, please cite our article in the
8 | Journal of Open Source Software.
9 | preferred-citation:
10 | authors:
11 | - family-names: Bussemaker
12 | given-names: Jasper H.
13 | orcid: "https://orcid.org/0000-0002-5421-6419"
14 | date-published: 2023-09-07
15 | doi: 10.21105/joss.05564
16 | issn: 2475-9066
17 | issue: 89
18 | journal: Journal of Open Source Software
19 | publisher:
20 | name: Open Journals
21 | start: 5564
22 | title: "SBArchOpt: Surrogate-Based Architecture Optimization"
23 | type: article
24 | url: "https://joss.theoj.org/papers/10.21105/joss.05564"
25 | volume: 8
26 | title: "SBArchOpt: Surrogate-Based Architecture Optimization"
27 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/test_constrained.py:
--------------------------------------------------------------------------------
1 | from sb_arch_opt.problems.constrained import *
2 | from sb_arch_opt.tests.problems.test_md_mo import run_test_no_hierarchy
3 |
4 |
5 | def test_cantilevered_beam():
6 | run_test_no_hierarchy(ArchCantileveredBeam())
7 |
8 |
9 | def test_welded_beam():
10 | run_test_no_hierarchy(ArchWeldedBeam())
11 |
12 |
13 | def test_md_welded_beam():
14 | run_test_no_hierarchy(MDWeldedBeam())
15 |
16 |
17 | def test_carside():
18 | run_test_no_hierarchy(ArchCarside())
19 |
20 |
21 | def test_md_carside():
22 | run_test_no_hierarchy(MDCarside())
23 |
24 |
25 | def test_osy():
26 | run_test_no_hierarchy(ArchOSY())
27 |
28 |
29 | def test_md_osy():
30 | run_test_no_hierarchy(MDOSY())
31 |
32 |
33 | def test_das_cmop():
34 | run_test_no_hierarchy(MODASCMOP(), exh_n_cont=1)
35 |
36 |
37 | def test_md_das_cmop():
38 | run_test_no_hierarchy(MDDASCMOP(), exh_n_cont=-1)
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023, Jasper Bussemaker.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | # https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
2 | name: Publish to PyPI
3 |
4 | on:
5 | release:
6 | types: [published]
7 |
8 | permissions:
9 | contents: read
10 |
11 | jobs:
12 | publish:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v6
17 | - name: Set up Python
18 | uses: actions/setup-python@v6
19 | with:
20 | python-version: '3.10'
21 |
22 | - name: Install dependencies
23 | run: |
24 | python -m pip install --upgrade pip
25 | pip install build
26 | - name: Build package
27 | run: python -m build
28 |
29 | - name: Publish package to PyPI
30 | # if: ${{ startsWith(github.ref, 'refs/tags/v') }}
31 | uses: pypa/gh-action-pypi-publish@release/v1
32 | with:
33 | password: ${{ secrets.PYPI_API_TOKEN }}
34 |
35 | # - name: Publish package to Test PyPI
36 | # uses: pypa/gh-action-pypi-publish@release/v1
37 | # with:
38 | # password: ${{ secrets.TEST_PYPI_API_TOKEN }}
39 | # repository-url: https://test.pypi.org/legacy/
40 |
--------------------------------------------------------------------------------
/docs/algo/smarty.md:
--------------------------------------------------------------------------------
1 | # SMARTy: Surrogate Modeling for Aero-Data Toolbox
2 |
3 | SMARTy is a surrogate modeling toolbox with optimization capabilities developed by the DLR. For more information refer to:
4 |
5 | Bekemeyer, P., Bertram, A., Hines Chaves, D.A., Dias Ribeiro, M., Garbo, A., Kiener, A., Sabater, C., Stradtner, M.,
6 | Wassing, S., Widhalm, M. and Goertz, S., 2022. Data-Driven Aerodynamic Modeling Using the DLR SMARTy Toolbox.
7 | In AIAA Aviation 2022 Forum (p. 3899). DOI: [10.2514/6.2022-3899](https://arc.aiaa.org/doi/abs/10.2514/6.2022-3899)
8 |
9 | ## Installation
10 |
11 | SMARTy is not openly available.
12 |
13 | ## Usage
14 |
15 | [API Reference](../api/smarty.md)
16 |
17 | The `get_smarty_optimizer` function can be used to get an interface object for running the optimization.
18 |
19 | ```python
20 | from sb_arch_opt.algo.smarty_interface import get_smarty_optimizer
21 |
22 | problem = ... # Subclass of ArchOptProblemBase
23 |
24 | # Get the interface and optimization loop
25 | smarty = get_smarty_optimizer(problem, n_init=100, n_infill=50)
26 |
27 | # Run the optimization loop
28 | smarty.optimize()
29 |
30 | # Extract data as a pymoo Population object
31 | pop = smarty.pop
32 | ```
33 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/test_tutorials.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import testbook
4 | from testbook.client import TestbookNotebookClient
5 |
6 | _docs_path = f'{os.path.dirname(__file__)}/../../docs'
7 | _t = 60*20
8 |
9 |
10 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
11 | @testbook.testbook(f'{_docs_path}/tutorial.ipynb', execute=False, timeout=_t)
12 | def test_tutorial(tb: TestbookNotebookClient):
13 | code_cells = []
14 | for cell in tb.cells:
15 | if cell.cell_type == 'code':
16 | code_cells.append(cell)
17 |
18 | # Set less infills to reduce testing time
19 | sbo_example_cell = code_cells[1]
20 | code = sbo_example_cell.source.split('\n')
21 | for i, line in enumerate(code):
22 | if line.startswith('n_infill'):
23 | code[i] = 'n_infill = 2'
24 | break
25 | sbo_example_cell.source = '\n'.join(code)
26 |
27 | tb.execute()
28 |
29 |
30 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
31 | @testbook.testbook(f'{_docs_path}/tutorial_tunable_meta_problem.ipynb', execute=True, timeout=_t)
32 | def test_tunable_hierarchical_meta_problem_tutorial(tb):
33 | pass
34 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3 |
4 | name: Tests
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main", "dev" ]
11 |
12 | jobs:
13 | test:
14 | name: Tests
15 | runs-on: ${{ matrix.os }}
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | os: [ubuntu-latest, windows-latest]
20 | python-version: ["3.12"]
21 | include:
22 | - os: ubuntu-latest
23 | python-version: "3.10"
24 |
25 | steps:
26 | - uses: actions/checkout@v6
27 | - name: Set up python
28 | uses: actions/setup-python@v6
29 | with:
30 | python-version: ${{ matrix.python-version }}
31 |
32 | - name: Python info
33 | run: python --version
34 |
35 | - name: Install dependencies
36 | run: |
37 | python -m pip install --upgrade pip setuptools
38 | pip install -r requirements-tests.txt
39 | pip install -r requirements-assignment.txt
40 | pip install -e .[arch_sbo,botorch,rocket,egor]
41 |
42 | - name: Test with pytest
43 | run: pytest -v sb_arch_opt --durations=10
44 |
--------------------------------------------------------------------------------
/docs/algo/hebo.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # HEBO: Heteroscedastic Evolutionary Bayesian Optimization
4 |
5 | [HEBO](https://hebo.readthedocs.io/en/) is a Bayesian optimization algorithm developed by Huawei Noah's Ark lab.
6 | It supports mixed-discrete parameter and several types of underlying probabilistic models.
7 |
8 | For more information:
9 | Cowen-Rivers, A.I., et al. 2022. HEBO: pushing the limits of sample-efficient hyper-parameter optimisation. Journal of
10 | Artificial Intelligence Research, 74, pp.1269-1349, DOI: [10.1613/jair.1.13643](https://dx.doi.org/10.1613/jair.1.13643)
11 |
12 | ## Installation
13 |
14 | ```
15 | pip install sb-arch-opt[hebo]
16 | ```
17 |
18 | ## Usage
19 |
20 | [API Reference](../api/hebo.md)
21 |
22 | The `get_hebo_optimizer` function can be used to get an interface object for running the optimization.
23 | The `hebo` object also has an ask-tell interface if needed.
24 |
25 | ```python
26 | from sb_arch_opt.algo.hebo_interface import get_hebo_optimizer
27 |
28 | problem = ... # Subclass of ArchOptProblemBase
29 |
30 | # Get the interface and optimization loop
31 | hebo = get_hebo_optimizer(problem, n_init=100, seed=42)
32 |
33 | # Run the optimization loop
34 | hebo.optimize(n_infill=50)
35 |
36 | # Extract data as a pymoo Population object
37 | pop = hebo.pop
38 | ```
39 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/algo/test_smarty.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from sb_arch_opt.problem import *
3 | from sb_arch_opt.algo.smarty_interface import *
4 | from sb_arch_opt.problems.continuous import Branin
5 | from sb_arch_opt.problems.constrained import ArchCantileveredBeam
6 | from sb_arch_opt.algo.smarty_interface.algo import SMARTyArchOptInterface
7 |
8 | def check_dependency():
9 | return pytest.mark.skipif(not HAS_SMARTY, reason='SMARTy dependencies not installed')
10 |
11 |
12 | @check_dependency()
13 | def test_opt_prob(problem: ArchOptProblemBase):
14 | smarty = SMARTyArchOptInterface(problem, n_init=10, n_infill=1)
15 | opt_prob = smarty.opt_prob
16 | assert opt_prob.bounds.shape[0] == problem.n_var
17 |
18 |
19 | @check_dependency()
20 | def test_simple():
21 | assert HAS_SMARTY
22 |
23 | smarty = get_smarty_optimizer(Branin(), n_init=10, n_infill=2)
24 | smarty.optimize()
25 |
26 | pop = smarty.pop
27 | assert len(pop) == 12
28 |
29 |
30 | @check_dependency()
31 | def test_simple_mo(problem: ArchOptProblemBase):
32 | assert HAS_SMARTY
33 |
34 | smarty = get_smarty_optimizer(problem, n_init=10, n_infill=2)
35 | smarty.optimize()
36 |
37 | pop = smarty.pop
38 | assert len(pop) == 12
39 |
40 |
41 | @check_dependency()
42 | def test_constrained():
43 | smarty = get_smarty_optimizer(ArchCantileveredBeam(), n_init=10, n_infill=1)
44 | smarty.optimize()
45 | assert len(smarty.pop) == 11
46 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/segomoe_interface/api.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | from sb_arch_opt.algo.segomoe_interface.algo import *
26 | from sb_arch_opt.algo.segomoe_interface.pymoo_algo import *
27 |
28 | __all__ = ['HAS_SEGOMOE', 'HAS_SMT', 'SEGOMOEInterface', 'SEGOMOEAlgorithm']
29 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/algo/test_botorch.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | from sb_arch_opt.problem import *
4 | from sb_arch_opt.algo.botorch_interface import *
5 | from sb_arch_opt.problems.discrete import MDBranin
6 | try:
7 | from ax.service.utils.best_point import get_pareto_optimal_parameters
8 | from botorch.exceptions.errors import InputDataError
9 | except ImportError:
10 | pass
11 |
12 | def check_dependency():
13 | return pytest.mark.skipif(not HAS_BOTORCH, reason='BoTorch/Ax dependencies not installed')
14 |
15 |
16 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
17 | def test_slow_tests():
18 | assert HAS_BOTORCH
19 |
20 |
21 | @check_dependency()
22 | def test_simple(problem: ArchOptProblemBase):
23 | interface = get_botorch_interface(problem)
24 | opt = interface.get_optimization_loop(n_init=10, n_infill=1, seed=42)
25 | opt.full_run()
26 |
27 | pop = interface.get_population(opt)
28 | assert len(pop) == 11
29 |
30 |
31 | @check_dependency()
32 | def test_simple_so():
33 | opt = get_botorch_interface(MDBranin()).get_optimization_loop(n_init=10, n_infill=1)
34 | opt.full_run()
35 |
36 |
37 | @check_dependency()
38 | def test_simple_failing(failing_problem: ArchOptProblemBase):
39 | interface = get_botorch_interface(failing_problem)
40 | opt = interface.get_optimization_loop(n_init=10, n_infill=1)
41 | opt.full_run()
42 |
43 | pop = interface.get_population(opt)
44 | assert len(pop) == 10
45 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/algo/test_hebo.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | from sb_arch_opt.problem import *
4 | from sb_arch_opt.algo.hebo_interface import *
5 | from sb_arch_opt.problems.md_mo import MOZDT1
6 | from sb_arch_opt.problems.constrained import ArchCantileveredBeam
7 | from sb_arch_opt.algo.hebo_interface.algo import HEBOArchOptInterface
8 |
9 | def check_dependency():
10 | return pytest.mark.skipif(not HAS_HEBO, reason='HEBO dependencies not installed')
11 |
12 |
13 | # @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
14 | # def test_slow_tests():
15 | # assert HAS_HEBO
16 |
17 |
18 | @check_dependency()
19 | def test_design_space(problem: ArchOptProblemBase):
20 | hebo = HEBOArchOptInterface(problem, n_init=10)
21 | design_space = hebo.design_space
22 | assert len(design_space.paras) == problem.n_var
23 |
24 |
25 | @check_dependency()
26 | def test_simple():
27 | assert HAS_HEBO
28 |
29 | n_init = 30
30 | hebo = get_hebo_optimizer(MOZDT1(), n_init=30, seed=42)
31 | hebo.optimize(n_infill=2)
32 |
33 | pop = hebo.pop
34 | assert len(pop) == n_init+2
35 |
36 |
37 | @check_dependency()
38 | def test_constrained():
39 | opt = get_hebo_optimizer(ArchCantileveredBeam(), n_init=20)
40 | opt.optimize(n_infill=1)
41 | assert len(opt.pop) == 21
42 |
43 |
44 | @check_dependency()
45 | def test_simple_failing(failing_problem: ArchOptProblemBase):
46 | hebo = get_hebo_optimizer(failing_problem, n_init=20)
47 | hebo.optimize(n_infill=1)
48 |
49 | pop = hebo.pop
50 | assert len(pop) == 10
51 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/botorch_interface/api.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | from sb_arch_opt.problem import ArchOptProblemBase
26 | from sb_arch_opt.algo.botorch_interface.algo import HAS_BOTORCH, AxInterface
27 |
28 | __all__ = ['HAS_BOTORCH', 'get_botorch_interface']
29 |
30 |
31 | def get_botorch_interface(problem: ArchOptProblemBase):
32 | """Gets the main interface to Ax and BoTorch"""
33 | return AxInterface(problem)
34 |
--------------------------------------------------------------------------------
/docs/algo/botorch.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # BoTorch: Bayesian Optimization with PyTorch
4 |
5 | [BoTorch](https://botorch.org/) is a Bayesian optimization framework written on top of the [PyTorch](https://pytorch.org/)
6 | machine learning library. More information:
7 |
8 | Bandalat, M. et al, "BoTorch: A Framework for Efficient Monte-Carlo Bayesian Optimization", https://arxiv.org/abs/1910.06403
9 |
10 | The framework is mostly interacted with through [Ax](https://ax.dev/).
11 |
12 | ## Installation
13 |
14 | ```
15 | pip install sb-arch-opt[botorch]
16 | ```
17 |
18 | ## Usage
19 |
20 | [API Reference](../api/botorch.md)
21 |
22 | The `get_botorch_interface` function can be used to get an interface object that can be used to create an
23 | `OptimizationLoop` instance, with correctly configured search space, optimization configuration, and evaluation
24 | function.
25 |
26 | Ax will take care of selecting the best underlying Bayesian (GP) model for the defined optimization problem. Note that
27 | it will always be some Gaussian Process model and therefore can be relatively expensive.
28 |
29 | ```python
30 | from sb_arch_opt.algo.botorch_interface import get_botorch_interface
31 |
32 | problem = ... # Subclass of ArchOptProblemBase
33 |
34 | # Get the interface and optimization loop
35 | interface = get_botorch_interface(problem)
36 | opt_loop = interface.get_optimization_loop(n_init=100, n_infill=50, seed=42)
37 |
38 | # Run the optimization loop until completion
39 | opt_loop.full_run()
40 |
41 | # Extract data as a pymoo Population object
42 | pop = interface.get_population(opt_loop)
43 | ```
44 |
--------------------------------------------------------------------------------
/.github/workflows/tests_basic.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3 |
4 | name: Basic Tests
5 |
6 | on:
7 | push:
8 | branches: [ "main", "dev" ]
9 | pull_request:
10 | branches: [ "main", "dev" ]
11 |
12 | jobs:
13 | test:
14 | name: Basic Tests
15 | runs-on: ${{ matrix.os }}
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | os: [ubuntu-latest]
20 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
21 |
22 | steps:
23 | - uses: actions/checkout@v6
24 | - name: Set up python
25 | uses: actions/setup-python@v6
26 | with:
27 | python-version: ${{ matrix.python-version }}
28 |
29 | - name: Python info
30 | run: python --version
31 |
32 | - name: Install dependencies
33 | run: |
34 | python -m pip install --upgrade pip setuptools
35 | pip install -r requirements-tests.txt
36 | pip install -e .
37 |
38 | - name: List dependencies
39 | run: |
40 | pip freeze > requirements-all.txt
41 | - name: Check dependency licenses
42 | id: license_check_report
43 | uses: pilosus/action-pip-license-checker@v3
44 | with:
45 | requirements: 'requirements-all.txt'
46 | fail: 'StrongCopyleft'
47 | - name: Print license report
48 | if: ${{ always() }}
49 | run: echo "${{ steps.license_check_report.outputs.report }}"
50 |
51 | - name: Test with pytest
52 | run: pytest -v sb_arch_opt --durations=10
53 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/hebo_interface/api.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | from sb_arch_opt.problem import ArchOptProblemBase
26 | from sb_arch_opt.algo.hebo_interface.algo import *
27 |
28 |
29 | __all__ = ['get_hebo_optimizer', 'HAS_HEBO']
30 |
31 |
32 | def get_hebo_optimizer(problem: ArchOptProblemBase, n_init: int, seed: int = None):
33 | """
34 | Gets the main interface to HEBO. Use the `optimize` method to run the DOE and infill loops.
35 | """
36 | check_dependencies()
37 | return HEBOArchOptInterface(problem, n_init, seed=seed)
38 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/smarty_interface/api.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | from sb_arch_opt.problem import ArchOptProblemBase
26 | from sb_arch_opt.algo.smarty_interface.algo import *
27 |
28 |
29 | __all__ = ['get_smarty_optimizer', 'HAS_SMARTY']
30 |
31 |
32 | def get_smarty_optimizer(problem: ArchOptProblemBase, n_init: int, n_infill: int):
33 | """
34 | Gets the main interface to SMARTy. Use the `optimize` method to run the DOE and infill loops.
35 | """
36 | check_dependencies()
37 | return SMARTyArchOptInterface(problem, n_init, n_infill)
38 |
--------------------------------------------------------------------------------
/sb_arch_opt/util.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging.config
3 | from appdirs import user_cache_dir
4 |
5 | _debug_log_captured = False
6 |
7 |
8 | def _prevent_capture():
9 | global _debug_log_captured
10 | _debug_log_captured = True
11 |
12 |
13 | def capture_log(level='INFO'):
14 | """Displays logging output in the console"""
15 | global _debug_log_captured
16 | if _debug_log_captured:
17 | return
18 | if level == 'DEBUG':
19 | _debug_log_captured = True
20 |
21 | logging.config.dictConfig({
22 | 'version': 1,
23 | 'disable_existing_loggers': False,
24 | 'formatters': {
25 | 'console': {
26 | 'format': '%(levelname)- 8s %(asctime)s %(name)- 18s: %(message)s'
27 | },
28 | },
29 | 'handlers': {
30 | 'console': {
31 | 'level': level,
32 | 'class': 'logging.StreamHandler',
33 | 'formatter': 'console',
34 | },
35 | },
36 | 'loggers': {
37 | 'sb_arch_opt': {
38 | 'handlers': ['console'],
39 | 'level': level,
40 | },
41 | },
42 | })
43 |
44 |
45 | def set_global_random_seed(seed: int = None):
46 | import random
47 | random.seed(seed)
48 |
49 | import numpy as np
50 | np.random.seed(seed)
51 |
52 |
53 | def get_np_random_singleton():
54 | from scipy._lib._util import check_random_state
55 | return check_random_state(seed=None)
56 |
57 |
58 | def get_cache_path(sub_path: str = None):
59 | path = user_cache_dir('SBArchOpt')
60 | os.makedirs(path, exist_ok=True)
61 | if sub_path is not None:
62 | path = os.path.join(path, sub_path)
63 | return path
64 |
--------------------------------------------------------------------------------
/docs/algo/tpe.md:
--------------------------------------------------------------------------------
1 | # Tree-structured Parzen Estimator (TPE) Algorithm
2 |
3 | A TPE inverts the typical prediction process of a surrogate model: it models x for given y. This allows it to model
4 | very complicated design spaces structures, making it appropriate for architecture optimization and hyperparameter
5 | optimization where it was first developed. For more details, refer to:
6 |
7 | Bergstra et al., "Algorithms for Hyper-Parameter Optimization", 2011, available
8 | [here](https://papers.nips.cc/paper/2011/file/86e8f7ab32cfd12577bc2619bc635690-Paper.pdf).
9 |
10 | We use the implementation found [here](https://github.com/nabenabe0928/tpe), which currently supports single-objective
11 | unconstrained optimization problems.
12 |
13 | ## Installation
14 |
15 | ```
16 | pip install sb-arch-opt[tpe]
17 | ```
18 |
19 | ## Usage
20 |
21 | The algorithm is implemented as a [pymoo](https://pymoo.org/) algorithm that already includes relevant architecture
22 | optimization measures. It can be used directly with pymoo's interface:
23 |
24 | ```python
25 | from pymoo.optimize import minimize
26 | from sb_arch_opt.algo.tpe_interface import *
27 |
28 | problem = ... # Subclass of ArchOptProblemBase
29 |
30 | # Enable intermediate results storage
31 | results_folder_path = 'path/to/results/folder'
32 |
33 | # Get TPE algorithm
34 | n_init = 100
35 | algo = TPEAlgorithm(n_init=n_init, results_folder=results_folder_path)
36 |
37 | # Start from previous results (skipped if no previous results are available)
38 | if initialize_from_previous_results(algo, problem, results_folder_path):
39 | # No need to evaluate any initial points, as they already have been evaluated
40 | n_init = 0
41 |
42 | n_infill = 10
43 | result = minimize(problem, algo, termination=('n_eval', n_init + n_infill))
44 | ```
45 |
--------------------------------------------------------------------------------
/docs/api/problem.md:
--------------------------------------------------------------------------------
1 | # Problem Definition and Sampling
2 |
3 | ::: sb_arch_opt.problem.ArchOptProblemBase
4 | handler: python
5 | options:
6 | heading_level: 2
7 | members:
8 | - design_space
9 | - correct_x
10 | - load_previous_results
11 | - store_results
12 | - evaluate
13 | - vars
14 | - get_categorical_values
15 | - is_conditionally_active
16 | - all_discrete_x
17 | - print_stats
18 | - get_imputation_ratio
19 | - get_correction_ratio
20 | - get_discrete_rates
21 | - get_failure_rate
22 | - get_n_declared_discrete
23 | - get_n_valid_discrete
24 | - get_n_correct_discrete
25 | - _arch_evaluate
26 | - _correct_x
27 |
28 | ::: sb_arch_opt.sampling.HierarchicalSampling
29 | handler: python
30 | options:
31 | heading_level: 2
32 | members:
33 | - sample_get_x
34 |
35 | ::: sb_arch_opt.design_space.ArchDesignSpace
36 | handler: python
37 | options:
38 | heading_level: 2
39 | members:
40 | - all_discrete_x
41 | - correct_x
42 | - corrector
43 | - quick_sample_discrete_x
44 | - des_vars
45 | - imputation_ratio
46 | - correction_ratio
47 | - is_conditionally_active
48 | - is_cat_mask
49 | - is_cont_mask
50 | - is_discrete_mask
51 | - is_int_mask
52 | - xl
53 | - xu
54 | - get_categorical_values
55 | - get_discrete_rates
56 | - get_n_declared_discrete
57 | - get_n_valid_discrete
58 | - get_n_correct_discrete
59 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/trieste_interface/api.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | from sb_arch_opt.problem import ArchOptProblemBase
26 | from sb_arch_opt.algo.trieste_interface.algo import *
27 |
28 |
29 | __all__ = ['get_trieste_optimizer', 'HAS_TRIESTE']
30 |
31 |
32 | def get_trieste_optimizer(problem: ArchOptProblemBase, n_init: int, n_infill: int, pof: float = .5, seed: int = None):
33 | """
34 | Gets the main interface to Trieste. Use the `run_optimization` method to run the DOE and infill loops.
35 | """
36 | check_dependencies()
37 | return ArchOptBayesianOptimizer(problem, n_init, n_infill, pof=pof, seed=seed)
38 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: SBArchOpt
2 | site_url: https://sbarchopt.readthedocs.io/
3 | repo_url: https://github.com/jbussemaker/SBArchOpt
4 | docs_dir: docs
5 | copyright: © 2024, Deutsches Zentrum für Luft- und Raumfahrt e.V.
6 | theme:
7 | name: material
8 | logo: icon.png
9 | favicon: icon.png
10 | palette:
11 | scheme: default
12 | primary: custom
13 | features:
14 | - navigation.tabs
15 | custom_dir: docs/overrides
16 | extra_css:
17 | - style.css
18 |
19 | nav:
20 | - 'Overview': index.md
21 | - Optimization:
22 | - 'pymoo': 'algo/pymoo.md'
23 | - 'ArchSBO': 'algo/arch_sbo.md'
24 | - 'BoTorch (Ax)': 'algo/botorch.md'
25 | - 'Trieste': 'algo/trieste.md'
26 | - 'HEBO': 'algo/hebo.md'
27 | - 'SEGOMOE': 'algo/segomoe.md'
28 | - 'SMARTy': 'algo/smarty.md'
29 | - Tutorials:
30 | - SBArchOpt: tutorial.ipynb
31 | - 'Test Problems': 'test_problems.md'
32 | - API Reference:
33 | - Problem Definition: 'api/problem.md'
34 | - pymoo: 'api/pymoo.md'
35 | - ArchSBO: 'api/arch_sbo.md'
36 | - 'BoTorch (Ax)': 'api/botorch.md'
37 | - 'Trieste': 'api/trieste.md'
38 | - 'HEBO': 'api/hebo.md'
39 | - 'SEGOMOE': 'api/segomoe.md'
40 | - 'SMARTy': 'api/smarty.md'
41 |
42 | markdown_extensions:
43 | - pymdownx.highlight:
44 | anchor_linenums: true
45 | line_spans: __span
46 | pygments_lang_class: true
47 | - pymdownx.inlinehilite
48 | - pymdownx.snippets
49 | - pymdownx.superfences
50 |
51 | plugins:
52 | - mkdocs-jupyter
53 | - mkdocstrings:
54 | handlers:
55 | python:
56 | options:
57 | allow_inspection: true
58 | show_root_heading: true
59 | show_source: false
60 | show_bases: false
61 | show_signature_annotations: true
62 | merge_init_into_class: true
63 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/egor_interface/api.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Onera
5 | Contact: remi.lafage@onera.fr
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | from sb_arch_opt.problem import ArchOptProblemBase
26 | from sb_arch_opt.algo.egor_interface.algo import *
27 |
28 | __all__ = ["HAS_EGOBOX", "get_egor_optimizer"]
29 |
30 |
31 | def get_egor_optimizer(
32 | problem: ArchOptProblemBase,
33 | n_init: int,
34 | results_folder: "None|str" = None,
35 | **kwargs
36 | ):
37 | """
38 | Gets the main interface to Egor.
39 |
40 | Use the `minimize` method to run the DOE and infill loops.
41 | `kwargs` arguments are directly pass to the native Egor object.
42 | See help(egobox.Egor) for more information
43 | """
44 | check_dependencies()
45 | return EgorArchOptInterface(problem, n_init, results_folder, **kwargs)
46 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/algo/test_tpe.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import tempfile
4 | from pymoo.optimize import minimize
5 | from sb_arch_opt.algo.tpe_interface import *
6 | from sb_arch_opt.problems.discrete import MDBranin
7 | from sb_arch_opt.problems.hidden_constraints import Alimo
8 |
9 |
10 | def check_dependency():
11 | return pytest.mark.skipif(not HAS_TPE, reason='TPE dependencies not installed')
12 |
13 |
14 | # @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
15 | # def test_slow_tests():
16 | # assert HAS_TPE
17 |
18 |
19 | @check_dependency()
20 | def test_simple():
21 | assert HAS_TPE
22 |
23 | tpe = ArchTPEInterface(MDBranin())
24 | x, f = tpe.optimize(n_init=1, n_infill=1)
25 | assert x.shape == (2, 4)
26 | assert f.shape == (2,)
27 |
28 |
29 | @check_dependency()
30 | def test_md_branin():
31 | tpe = ArchTPEInterface(MDBranin())
32 | x, f = tpe.optimize(n_init=20, n_infill=10)
33 | assert x.shape == (30, 4)
34 | assert f.shape == (30,)
35 |
36 |
37 | @check_dependency()
38 | def test_algorithm():
39 | algo = TPEAlgorithm(n_init=20)
40 | result = minimize(MDBranin(), algo, ('n_eval', 30), copy_algorithm=False)
41 | assert len(result.pop) == 30
42 | assert algo.opt is not None
43 |
44 |
45 | @check_dependency()
46 | def test_failed_evaluations():
47 | minimize(Alimo(), TPEAlgorithm(n_init=20), ('n_eval', 100))
48 |
49 |
50 | @check_dependency()
51 | def test_store_results_restart():
52 | problem = MDBranin()
53 | with tempfile.TemporaryDirectory() as tmp_folder:
54 | for i in range(2):
55 | tpe = TPEAlgorithm(n_init=10, results_folder=tmp_folder)
56 | initialize_from_previous_results(tpe, problem, tmp_folder)
57 |
58 | n_eval = 11+i
59 | result = minimize(problem, tpe, termination=('n_eval', n_eval))
60 | assert len(result.pop) == 10+(i+1)
61 |
--------------------------------------------------------------------------------
/.github/workflows/tests_slow.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3 |
4 | name: Slow Tests
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main", "dev" ]
11 | schedule:
12 | - cron: "21 3 * * 1"
13 |
14 | jobs:
15 | test:
16 | name: Slow Tests
17 | runs-on: ${{ matrix.os }}
18 | strategy:
19 | fail-fast: false
20 | matrix:
21 | os: [ubuntu-latest]
22 | python-version: ["3.12"]
23 |
24 | steps:
25 | - uses: actions/checkout@v6
26 | - name: Set up python
27 | uses: actions/setup-python@v6
28 | with:
29 | python-version: ${{ matrix.python-version }}
30 |
31 | - name: Python info
32 | run: |
33 | python --version
34 | df -h
35 |
36 | - name: Install dependencies
37 | run: |
38 | python -m pip install --upgrade pip setuptools
39 | pip install -r requirements-tests.txt
40 | pip install -r requirements-assignment.txt
41 | pip install -r requirements-ota.txt
42 | pip install -e .[arch_sbo,botorch,rocket,egor]
43 | pip install jupyter ipython ipykernel
44 | ipython kernel install --name "python3" --user
45 | df -h
46 |
47 | - name: List dependencies
48 | run: |
49 | pip freeze > requirements-all.txt
50 | - name: Check dependency licenses
51 | id: license_check_report
52 | uses: pilosus/action-pip-license-checker@v3
53 | with:
54 | requirements: 'requirements-all.txt'
55 | fail: 'StrongCopyleft'
56 | - name: Print license report
57 | if: ${{ always() }}
58 | run: echo "${{ steps.license_check_report.outputs.report }}"
59 |
60 | - name: Test with pytest
61 | run: RUN_SLOW_TESTS=1 pytest -v sb_arch_opt --durations=20
62 |
--------------------------------------------------------------------------------
/docs/algo/trieste.md:
--------------------------------------------------------------------------------
1 | # Trieste
2 |
3 | [Trieste](https://secondmind-labs.github.io/trieste/1.0.0/index.html) is a Bayesian optimization library built on
4 | [TensorFlow](https://www.tensorflow.org/), Google's machine learning framework. Trieste is an evolution of spearmint.
5 |
6 | Trieste supports constrained, multi-objective, noisy, multi-fidelity optimization subject to hidden constraints (called
7 | failed regions in the documentation).
8 |
9 | For more information:
10 | Picheny et al., "Trieste: Efficiently Exploring The Depths of Black-box Functions with TensorFlow", 2023,
11 | [arXiv:2302.08436](https://arxiv.org/abs/2302.08436)
12 |
13 | ## Installation
14 |
15 | ```
16 | pip install sb-arch-opt[trieste]
17 | ```
18 |
19 | ## Usage
20 |
21 | [API Reference](../api/trieste.md)
22 |
23 | The `get_trieste_optimizer` function can be used to get an interface object that can be used to create an
24 | `ArchOptBayesianOptimizer` instance, with correctly configured search space, optimization configuration, evaluation
25 | function, and possibility to deal with and stay away from hidden constraints.
26 |
27 | To speed up the infill process if you are sure you won't have hidden constraints, you can let the
28 | `might_have_hidden_constraints` function of your problem class return False.
29 |
30 | ```python
31 | from sb_arch_opt.algo.trieste_interface import get_trieste_optimizer
32 |
33 | problem = ... # Subclass of ArchOptProblemBase
34 |
35 | # Get the interface and optimization loop
36 | optimizer = get_trieste_optimizer(problem, n_init=100, n_infill=50, seed=42)
37 |
38 | # Start from previous results (skipped if no previous results are available)
39 | results_folder_path = 'path/to/results/folder'
40 | optimizer.initialize_from_previous(results_folder_path)
41 |
42 | # Run the optimization loop (the results folder is optional)
43 | result = optimizer.run_optimization(results_folder=results_folder_path)
44 |
45 | # Extract data as a pymoo Population object
46 | pop = optimizer.to_population(result.datasets)
47 | ```
48 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/test_assignment.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import numpy as np
4 | from sb_arch_opt.problems.assignment import *
5 | from sb_arch_opt.tests.problems.test_hierarchical import run_test_hierarchy
6 |
7 | def check_dependency():
8 | return pytest.mark.skipif(not HAS_ASSIGN_ENC, reason='assign_enc dependencies not installed')
9 |
10 |
11 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
12 | def test_slow_tests():
13 | assert HAS_ASSIGN_ENC
14 |
15 |
16 | @check_dependency()
17 | def test_assignment():
18 | assignment = Assignment()
19 | run_test_hierarchy(assignment, 1)
20 | run_test_hierarchy(AssignmentLarge(), 1, check_n_valid=False)
21 |
22 | AssignmentInj().print_stats()
23 | AssignmentInjLarge().print_stats()
24 | AssignmentRepeat().print_stats()
25 | AssignmentRepeatLarge().print_stats()
26 |
27 | x_all, is_act_all = assignment.all_discrete_x
28 | assert x_all is not None
29 |
30 | is_cond_act = assignment.is_conditionally_active
31 | assert np.all(is_cond_act == np.any(~is_act_all, axis=0))
32 |
33 |
34 | @check_dependency()
35 | def test_partitioning():
36 | Partitioning().print_stats()
37 | run_test_hierarchy(PartitioningCovering(), 1.71, corr_ratio=1.71)
38 |
39 | _ = PartitioningCovering().is_conditionally_active
40 |
41 |
42 | @check_dependency()
43 | def test_unordered():
44 | run_test_hierarchy(UnordNonReplComb(), 2.55, corr_ratio=2.55)
45 | UnordNonReplCombLarge().print_stats()
46 | UnorderedComb().print_stats()
47 |
48 | _ = UnorderedComb().is_conditionally_active
49 |
50 |
51 | @check_dependency()
52 | def test_assign_enc_gnc():
53 | for problem, n_valid, imp_ratio, corr_ratio in [
54 | (AssignmentGNCNoActType(), 327, 14.1, 3.01),
55 | (AssignmentGNCNoAct(), 29857, 39.5, np.nan),
56 | (AssignmentGNCNoType(), 85779, 82.5, np.nan),
57 | (AssignmentGNC(), 79091323, 367, np.nan),
58 | ]:
59 | run_test_hierarchy(problem, imp_ratio, check_n_valid=n_valid < 400, corr_ratio=corr_ratio)
60 | assert problem.get_n_valid_discrete() == n_valid
61 |
62 | x_all, _ = problem.all_discrete_x
63 | _ = problem.is_conditionally_active
64 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/test_hidden_constraints.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | from sb_arch_opt.problems.hidden_constraints import *
4 | from sb_arch_opt.tests.problems.test_discrete import run_test_no_hierarchy
5 | from sb_arch_opt.tests.problems.test_hierarchical import run_test_hierarchy
6 |
7 |
8 | def test_mueller_01():
9 | run_test_no_hierarchy(Mueller01())
10 |
11 |
12 | def test_mueller_02():
13 | run_test_no_hierarchy(Mueller02())
14 | run_test_no_hierarchy(MDMueller02())
15 | run_test_hierarchy(HierMueller02(), 5.4, corr_ratio=1.04)
16 |
17 |
18 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
19 | def test_mueller_08():
20 | run_test_no_hierarchy(Mueller08())
21 | run_test_no_hierarchy(MOMueller08())
22 | run_test_no_hierarchy(MDMueller08())
23 | run_test_no_hierarchy(MDMOMueller08())
24 | run_test_hierarchy(HierMueller08(), 5.4, corr_ratio=1.04)
25 | run_test_hierarchy(MOHierMueller08(), 5.4, corr_ratio=1.04)
26 |
27 |
28 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
29 | def test_alimo():
30 | run_test_no_hierarchy(Alimo())
31 | run_test_no_hierarchy(AlimoEdge())
32 | run_test_hierarchy(HierAlimo(), 5.4, corr_ratio=1.04)
33 | run_test_hierarchy(HierAlimoEdge(), 5.4, corr_ratio=1.04)
34 |
35 |
36 | def test_hc_branin():
37 | run_test_no_hierarchy(HCBranin())
38 |
39 |
40 | def test_hc_sphere():
41 | run_test_no_hierarchy(HCSphere())
42 |
43 |
44 | def test_hier_rosenbrock_hc():
45 | run_test_hierarchy(MOHierarchicalRosenbrockHC(), 1.5)
46 |
47 |
48 | def test_hier_test_problem_hc():
49 | run_test_hierarchy(HCMOHierarchicalTestProblem(), 72)
50 |
51 |
52 | def test_cantilevered_beam():
53 | run_test_no_hierarchy(CantileveredBeamHC())
54 | run_test_no_hierarchy(MDCantileveredBeamHC())
55 |
56 |
57 | def test_carside():
58 | run_test_no_hierarchy(CarsideHC())
59 | run_test_no_hierarchy(CarsideHCLess())
60 | run_test_no_hierarchy(MDCarsideHC())
61 |
62 |
63 | def test_tfaily():
64 | run_test_no_hierarchy(Tfaily01())
65 | run_test_no_hierarchy(Tfaily02())
66 | run_test_no_hierarchy(Tfaily03())
67 | run_test_no_hierarchy(Tfaily04())
68 |
--------------------------------------------------------------------------------
/docs/algo/arch_sbo.md:
--------------------------------------------------------------------------------
1 | # Architecture Surrogate-Based Optimization (SBO) Algorithm
2 |
3 | ArchSBO implements a Surrogate-Based Optimization (SBO) algorithm configured for solving most types of architecture
4 | optimization problems. It is presented in the following work:
5 |
6 | J.H. Bussemaker et al. "Surrogate-Based Optimization of System Architectures Subject to Hidden Constraints".
7 | In: AIAA AVIATION 2024 FORUM. Las Vegas, NV, USA, July 2024.
8 | DOI: [10.2514/6.2024-4401](https://arc.aiaa.org/doi/10.2514/6.2024-4401)
9 |
10 | The algorithm uses state-of-the-art mixed-discrete hierarchical Gaussian Process (Kriging) surrogate models and ensures
11 | that all evaluated and selected infill points are imputed (and therefore valid).
12 | In case of presence of hidden constraints (i.e. simulation/evaluation failures), the failure area is predicted using
13 | a random forest classifier to ensure that only the viable area is explored.
14 |
15 | Either an RBF or a Kriging surrogate model is used. Kriging has as advantage that it also can predict the amount of
16 | uncertainty next to the model mean, and therefore allows for more interesting infill criteria. RBF, however, is faster
17 | to train and evaluate.
18 |
19 | ## Installation
20 |
21 | ```
22 | pip install sb-arch-opt[arch_sbo]
23 | ```
24 |
25 | ## Usage
26 |
27 | [API Reference](../api/arch_sbo.md)
28 |
29 | The algorithm is implemented as a [pymoo](https://pymoo.org/) algorithm and already includes all relevant architecture
30 | optimization measures. It can be used directly with pymoo's interface:
31 |
32 | ```python
33 | from pymoo.optimize import minimize
34 | from sb_arch_opt.algo.arch_sbo import get_arch_sbo_gp, get_arch_sbo_rbf
35 |
36 | problem = ... # Subclass of ArchOptProblemBase
37 |
38 | # Get Kriging or RBF algorithm
39 | n_init = 100
40 | results_folder_path = 'path/to/results/folder'
41 | gp_arch_sbo_algo = get_arch_sbo_gp(problem, init_size=n_init,
42 | results_folder=results_folder_path)
43 |
44 | # Start from previous results (skipped if no previous results are available)
45 | gp_arch_sbo_algo.initialize_from_previous_results(problem, results_folder_path)
46 |
47 | n_infill = 10
48 | result = minimize(problem, gp_arch_sbo_algo,
49 | termination=('n_eval', n_init + n_infill), seed=42) # Remove seed in production
50 | ```
51 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/algo/test_egor.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import tempfile
4 | from sb_arch_opt.problem import *
5 | from sb_arch_opt.algo.egor_interface import *
6 | from sb_arch_opt.problems.discrete import MDBranin
7 | from sb_arch_opt.problems.constrained import ArchCantileveredBeam, MDCantileveredBeam
8 | from sb_arch_opt.algo.egor_interface.algo import EgorArchOptInterface
9 |
10 | def check_dependency():
11 | return pytest.mark.skipif(not HAS_EGOBOX, reason="Egor dependencies not installed")
12 |
13 |
14 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
15 | def test_slow_tests():
16 | assert HAS_EGOBOX
17 |
18 |
19 | @check_dependency()
20 | def test_design_space(problem: ArchOptProblemBase):
21 | egor = EgorArchOptInterface(problem, n_init=10)
22 | design_space = egor.design_space
23 | assert len(design_space) == problem.n_var
24 |
25 |
26 | @check_dependency()
27 | def test_simple():
28 | assert HAS_EGOBOX
29 | n_init = 30
30 | n_infill = 2
31 | egor = get_egor_optimizer(MDBranin(), n_init=n_init, seed=42)
32 | egor.minimize(n_infill=n_infill)
33 | pop = egor.pop
34 | assert len(pop) == n_init + n_infill
35 |
36 |
37 | @check_dependency()
38 | def test_restart():
39 | with tempfile.TemporaryDirectory() as temp_dir:
40 | n_init = 30
41 | n_infill = 2
42 | egor = get_egor_optimizer(MDBranin(), n_init=n_init, results_folder=temp_dir, seed=42)
43 | egor.minimize(n_infill=n_infill)
44 | pop = egor.pop
45 | assert len(pop) == n_init + n_infill
46 |
47 | egor2 = get_egor_optimizer(MDBranin(), n_init=n_init, results_folder=temp_dir, seed=42)
48 | egor2.initialize_from_previous(results_folder=temp_dir)
49 | pop = egor2.pop
50 | assert len(pop) == n_init + n_infill
51 |
52 | n_infill2 = 10
53 | egor2.minimize(n_infill=n_infill2)
54 | pop = egor2.pop
55 | assert len(pop) == n_init + n_infill2
56 |
57 |
58 | @check_dependency()
59 | def test_constrained():
60 | opt = get_egor_optimizer(ArchCantileveredBeam(), n_init=20)
61 | opt.minimize(n_infill=1)
62 | assert len(opt.pop) == 21
63 |
64 |
65 | @check_dependency()
66 | def test_md_constrained():
67 | opt = get_egor_optimizer(MDCantileveredBeam(), n_init=20)
68 | opt.minimize(n_infill=1)
69 | assert len(opt.pop) == 21
70 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/algo/test_trieste.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import tempfile
4 | from sb_arch_opt.problem import *
5 | from sb_arch_opt.algo.trieste_interface import *
6 | from sb_arch_opt.problems.constrained import ArchCantileveredBeam
7 | from sb_arch_opt.algo.trieste_interface.algo import ArchOptBayesianOptimizer
8 |
9 |
10 | def check_dependency():
11 | return pytest.mark.skipif(not HAS_TRIESTE, reason='Trieste dependencies not installed')
12 |
13 |
14 | # @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
15 | # def test_slow_tests():
16 | # assert HAS_TRIESTE
17 |
18 |
19 | @check_dependency()
20 | def test_search_space(problem: ArchOptProblemBase):
21 | search_space = ArchOptBayesianOptimizer.get_search_space(problem)
22 | assert search_space.dimension == problem.n_var
23 |
24 |
25 | @check_dependency()
26 | def test_simple(problem: ArchOptProblemBase):
27 | assert HAS_TRIESTE
28 |
29 | opt = get_trieste_optimizer(problem, n_init=10, n_infill=1, seed=42)
30 | assert repr(opt)
31 | result = opt.run_optimization()
32 |
33 | pop = opt.to_population(result.datasets)
34 | assert len(pop) == 11
35 |
36 |
37 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
38 | @check_dependency()
39 | def test_constrained():
40 | opt = get_trieste_optimizer(ArchCantileveredBeam(), n_init=10, n_infill=1)
41 | assert opt.run_optimization()
42 |
43 |
44 | # @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
45 | @pytest.mark.skip('TensorFlow FuncGraph cannot be pickled')
46 | @check_dependency()
47 | def test_store_results_restart(problem: ArchOptProblemBase):
48 | with tempfile.TemporaryDirectory() as tmp_folder:
49 | for i in range(2):
50 | opt = get_trieste_optimizer(problem, n_init=10, n_infill=1+i)
51 | opt.initialize_from_previous(tmp_folder)
52 | result = opt.run_optimization(results_folder=tmp_folder)
53 |
54 | pop = opt.to_population(result.datasets)
55 | assert len(pop) == 11+i
56 |
57 |
58 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
59 | @check_dependency()
60 | def test_simple_failing(failing_problem: ArchOptProblemBase):
61 | opt = get_trieste_optimizer(failing_problem, n_init=10, n_infill=1)
62 | result = opt.run_optimization()
63 |
64 | pop = opt.to_population(result.datasets)
65 | assert len(pop) == 5
66 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/test_gnc.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from sb_arch_opt.problems.gnc import *
3 | from sb_arch_opt.sampling import HierarchicalSampling
4 | from sb_arch_opt.tests.problems.test_hierarchical import run_test_hierarchy
5 |
6 |
7 | def test_gnc():
8 | problem: GNCProblemBase
9 | for problem, n_valid, imp_ratio, corr_ratio in [
10 | (GNCNoActNrType(), 265, 1.93, 1.93),
11 | (GNCNoActType(), 327, 14.1, 3.01),
12 | (GNCNoActNr(), 26500, 14.1, 14.1),
13 | (GNCNoAct(), 29857, 112.5, 17.2),
14 |
15 | (GNCNoNrType(), 70225, 3.73, 3.73),
16 | (GNCNoType(), 85779, 82.5, 9.01),
17 | (GNCNoNr(), 70225000, 73.5, 73.5),
18 | (GNC(), 79091323, 1761, 123),
19 |
20 | (MDGNCNoActNr(), 265, 1.93, 1.93),
21 | (MDGNCNoAct(), 327, 14.1, 3.01),
22 | (SOMDGNCNoAct(), 327, 14.1, 3.01),
23 | (MDGNCNoNr(), 70225, 3.73, 3.73),
24 | (MDGNC(), 85779, 82.5, 9.01),
25 | ]:
26 | run_test_hierarchy(problem, imp_ratio, check_n_valid=n_valid < 400, corr_ratio=corr_ratio)
27 | assert problem.get_n_valid_discrete() == n_valid
28 |
29 | assert np.any(~problem.is_conditionally_active)
30 |
31 | x_rnd, _ = problem.design_space.quick_sample_discrete_x(300)
32 | x_corr, is_act_corr = problem.correct_x(x_rnd)
33 | x_corr_, is_act_corr_ = problem.correct_x(x_corr)
34 | assert np.all(x_corr == x_corr_)
35 | assert np.all(is_act_corr == is_act_corr_)
36 |
37 | pop = HierarchicalSampling().do(problem, 300)
38 | if np.all(problem.is_discrete_mask):
39 | assert len(pop) == min(n_valid, 300)
40 | else:
41 | # Mixed-discrete problems
42 | assert len(pop) == 300
43 | assert problem.get_continuous_imputation_ratio() > 1
44 | assert problem.get_imputation_ratio() > imp_ratio
45 | assert problem.get_continuous_correction_ratio() > 1
46 | assert problem.get_correction_ratio() > corr_ratio
47 |
48 | if not problem.choose_type or not problem.choose_nr:
49 | assert problem.get_continuous_correction_ratio() == problem.get_continuous_imputation_ratio()
50 | else:
51 | assert problem.get_continuous_correction_ratio() != problem.get_continuous_imputation_ratio()
52 |
53 |
54 | def test_gnc_cont_corr():
55 | problem = MDGNCNoActNr()
56 | assert np.all(problem.is_cont_mask[:6])
57 |
58 | x = np.array([problem.xl.copy()])
59 | x[0, :3] = [.5, .25, .75]
60 | x, is_active = problem.correct_x(x)
61 | assert np.all(x[0, :3] == [.5, .5, .75])
62 | assert np.all(is_active[0, :3])
63 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/pymoo_interface/random_search.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | import logging
26 | from sb_arch_opt.problem import *
27 | from sb_arch_opt.algo.pymoo_interface.api import ArchOptEvaluator
28 | from sb_arch_opt.algo.pymoo_interface.metrics import EHVMultiObjectiveOutput
29 | from sb_arch_opt.sampling import LargeDuplicateElimination, HierarchicalSampling
30 |
31 | from pymoo.core.algorithm import Algorithm
32 | from pymoo.core.population import Population
33 | from pymoo.util.optimum import filter_optimum
34 | from pymoo.core.initialization import Initialization
35 |
36 | __all__ = ['RandomSearchAlgorithm']
37 |
38 | log = logging.getLogger('sb_arch_opt.random')
39 |
40 |
41 | class RandomSearchAlgorithm(Algorithm):
42 | """
43 | A random search algorithm for benchmarking purposes.
44 | """
45 |
46 | def __init__(self, n_init: int, **kwargs):
47 | super().__init__(
48 | outputs=EHVMultiObjectiveOutput(),
49 | evaluator=ArchOptEvaluator(),
50 | **kwargs)
51 | self.n_init = n_init
52 | self.sampling = HierarchicalSampling()
53 | self.initialization = Initialization(
54 | self.sampling, repair=ArchOptRepair(), eliminate_duplicates=LargeDuplicateElimination())
55 |
56 | def _initialize_infill(self):
57 | return self.initialization.do(self.problem, self.n_init)
58 |
59 | def _initialize_advance(self, infills=None, **kwargs):
60 | self.pop = infills
61 |
62 | def _infill(self):
63 | return self.sampling.do(self.problem, 1)
64 |
65 | def _advance(self, infills=None, is_init=False, **kwargs):
66 | self.pop = Population.merge(self.pop, infills)
67 |
68 | def _set_optimum(self):
69 | pop = self.pop
70 | if self.opt is not None:
71 | pop = Population.merge(self.opt, pop)
72 | self.opt = filter_optimum(pop, least_infeasible=True)
73 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/test_md_mo.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from sb_arch_opt.sampling import *
3 | from sb_arch_opt.problems.md_mo import *
4 | from sb_arch_opt.problems.problems_base import MixedDiscretizerProblemBase
5 | from pymoo.problems.multi.zdt import ZDT1
6 | from pymoo.core.evaluator import Evaluator
7 |
8 |
9 | def test_md_base():
10 | problem = MixedDiscretizerProblemBase(ZDT1(n_var=4), n_opts=4, n_vars_int=2)
11 | assert problem.get_n_declared_discrete() == 4**2
12 | assert problem.get_imputation_ratio() == 1
13 | assert problem.get_correction_ratio() == 1
14 | problem.print_stats()
15 |
16 | pop = HierarchicalExhaustiveSampling(n_cont=3).do(problem, 0)
17 | assert len(pop) == (4**2)*(3**2)
18 | Evaluator().eval(problem, pop)
19 |
20 |
21 | def run_test_no_hierarchy(problem, exh_n_cont=3):
22 | assert problem.get_imputation_ratio() == 1
23 | assert problem.get_correction_ratio() == 1
24 | assert np.all(~problem.is_conditionally_active)
25 | problem.print_stats()
26 |
27 | x_discrete, is_act_discrete = problem.all_discrete_x
28 | if x_discrete is not None:
29 | assert x_discrete.shape[0] == problem.get_n_valid_discrete()
30 | if x_discrete.shape[0] < 1000:
31 | assert np.all(~LargeDuplicateElimination.eliminate(x_discrete))
32 |
33 | pop = None
34 | if exh_n_cont != -1 and (HierarchicalExhaustiveSampling.get_n_sample_exhaustive(problem, n_cont=exh_n_cont) < 1e3
35 | or x_discrete is not None):
36 | try:
37 | pop = HierarchicalExhaustiveSampling(n_cont=exh_n_cont).do(problem, 0)
38 | except MemoryError:
39 | pass
40 | if pop is None:
41 | pop = HierarchicalSampling().do(problem, 100)
42 | Evaluator().eval(problem, pop)
43 | problem.get_population_statistics(pop, show=True)
44 | return pop
45 |
46 |
47 | def test_mo_himmelblau():
48 | pop = run_test_no_hierarchy(MOHimmelblau())
49 | assert len(pop) == 3**2
50 |
51 |
52 | def test_md_mo_himmelblau():
53 | pop = run_test_no_hierarchy(MDMOHimmelblau())
54 | assert len(pop) == 10*3
55 |
56 |
57 | def test_discrete_mo_himmelblau():
58 | pop = run_test_no_hierarchy(DMOHimmelblau())
59 | assert len(pop) == 10*10
60 |
61 |
62 | def test_mo_goldstein():
63 | run_test_no_hierarchy(MOGoldstein())
64 |
65 |
66 | def test_md_mo_goldstein():
67 | run_test_no_hierarchy(MDMOGoldstein())
68 |
69 |
70 | def test_discrete_mo_goldstein():
71 | run_test_no_hierarchy(DMOGoldstein())
72 |
73 |
74 | def test_mo_rosenbrock():
75 | run_test_no_hierarchy(MORosenbrock())
76 |
77 |
78 | def test_md_mo_rosenbrock():
79 | run_test_no_hierarchy(MDMORosenbrock())
80 |
81 |
82 | def test_mo_zdt1():
83 | run_test_no_hierarchy(MOZDT1(), exh_n_cont=1)
84 |
85 |
86 | def test_md_mo_zdt1_small():
87 | run_test_no_hierarchy(MDZDT1Small(), exh_n_cont=1)
88 |
89 |
90 | def test_md_mo_zdt1_mid():
91 | run_test_no_hierarchy(MDZDT1Mid(), exh_n_cont=1)
92 |
93 |
94 | def test_md_mo_zdt1():
95 | run_test_no_hierarchy(MDZDT1(), exh_n_cont=1)
96 |
97 |
98 | def test_discrete_mo_zdt1():
99 | run_test_no_hierarchy(DZDT1(), exh_n_cont=1)
100 |
--------------------------------------------------------------------------------
/docs/algo/egor.md:
--------------------------------------------------------------------------------
1 | # Egor (from egobox library)
2 |
3 | Egor is a surrogate based optimizer provided with the [egobox](https://joss.theoj.org/papers/10.21105/joss.04737) library.
4 |
5 | Egor supports single-objective optimization subject to inequality constraints (<=0) with mixed discrete variables.
6 | The algorithm uses gaussian processes to iteratively approximate the function under minimization adaptively.
7 | It selects relevant points by minimising an infill criterion allowing to balance the exploitation and the exploration
8 | of design space regions where the minimum may be located.
9 |
10 | Egor comes with the following features:
11 | * management of mixed-discrete variables through continuous relaxation (integer, ordinal or categorical)
12 | * selection of an infill criterion: EI, WB2 (default), WB2S
13 | * possible use of a mixture of gaussian processes with different trend types and correlation kernels (default to single ordinary kriging)
14 | * reduction of the problem input dimension using partial least squared regression (aka KPLS, used when pb dim >= 9)
15 | * multithreaded implementation allowing to use multicore cpus (for surrogates training and internal multistart optimizations)
16 |
17 | It has been developed with experience from the following work:
18 |
19 | Bouhlel, M. A., Hwang, J. T., Bartoli, N., Lafage, R., Morlier, J., & Martins, J. R. R. A.
20 | (2019). [A python surrogate modeling framework with derivatives](https://doi.org/10.1016/j.advengsoft.2019.03.005).
21 | Advances in Engineering Software, 102662.
22 |
23 | Bartoli, N., Lefebvre, T., Dubreuil, S., Olivanti, R., Priem, R., Bons, N., Martins, J. R. R. A.,
24 | & Morlier, J. (2019). [Adaptive modeling strategy for constrained global optimization with application to aerodynamic wing design](https://doi.org/10.1016/j.ast.2019.03.041).
25 | Aerospace Science and Technology, 90, 85–102.
26 |
27 | Bouhlel, M. A., Bartoli, N., Otsmane, A., & Morlier, J. (2016). [Improving kriging surrogates of high-dimensional design models by partial least squares dimension reduction](https://doi.org/10.1007/s00158-015-1395-9). Structural and Multidisciplinary Optimization, 53(5), 935–952.
28 |
29 | ## Installation
30 |
31 | ```
32 | pip install sb-arch-opt[egor]
33 | ```
34 |
35 | ## Usage
36 |
37 | [API Reference](../api/egor.md)
38 |
39 | The `get_egor_optimizer` function can be used to get an interface object that can be used to create an
40 | `Egor` instance, with correctly configured search space, optimization configuration, evaluation
41 | function.
42 |
43 | This function allows to pass additional keyword arguments to the underlying Egor optimizer.
44 | See help(egobox.Egor) for further available options.
45 |
46 | ```python
47 | from sb_arch_opt.algo.egor_interface import get_egor_optimizer
48 |
49 | problem = ... # Subclass of ArchOptProblemBase
50 |
51 | # Get the interface and optimization loop
52 | egor = get_egor_optimizer(problem, n_init=100, seed=42)
53 |
54 | # Start from previous results (optional)
55 | results_folder_path = 'path/to/results/folder'
56 | egor.initialize_from_previous(results_folder_path)
57 |
58 | # Run the optimization loop (the results folder to store results is optional)
59 | result = egor.minimize(results_folder=results_folder_path)
60 |
61 | # Extract result as numpy arrays
62 | print(f"Minimum {result.y_opt} at {result.x_opt}")
63 | print(f"History {result.x_hist} {result.y_hist}")
64 |
65 | # Extract data as a pymoo Population object
66 | pop = egor.pop
67 | ```
68 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/test_rocket.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | from sb_arch_opt.problems.rocket import *
4 | from sb_arch_opt.problems.rocket_eval import *
5 | from sb_arch_opt.tests.problems.test_hierarchical import run_test_hierarchy
6 |
7 | def check_dependency():
8 | return pytest.mark.skipif(not HAS_ROCKET, reason='Rocket dependencies not installed')
9 |
10 |
11 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
12 | def test_slow_tests():
13 | assert HAS_ROCKET
14 |
15 |
16 | @check_dependency()
17 | def test_1_stage():
18 | rocket = Rocket(
19 | stages=[
20 | Stage(
21 | engines=[Engine.VULCAIN],
22 | length=26.47,
23 | ),
24 | ],
25 | head_shape=HeadShape.SPHERE,
26 | length_diameter_ratio=10.83,
27 | max_q=50e3,
28 | payload_density=2810.
29 | )
30 |
31 | performance = RocketEvaluator.evaluate(rocket)
32 | assert performance.cost == pytest.approx(53845830.)
33 | assert performance.payload_mass == pytest.approx(2783, abs=1)
34 | assert performance.delta_structural == pytest.approx(-23532, abs=1)
35 | assert performance.delta_payload == pytest.approx(-2.832, abs=1e-3)
36 |
37 |
38 | @check_dependency()
39 | def test_2_stages():
40 | rocket = Rocket(
41 | stages=[
42 | Stage(
43 | engines=[Engine.VULCAIN],
44 | length=20.37,
45 | ),
46 | Stage(
47 | engines=[Engine.VULCAIN],
48 | length=6.8,
49 | ),
50 | ],
51 | head_shape=HeadShape.SPHERE,
52 | length_diameter_ratio=11.32,
53 | max_q=50e3,
54 | payload_density=2810.
55 | )
56 |
57 | performance = RocketEvaluator.evaluate(rocket)
58 | assert performance.cost == pytest.approx(87563960.)
59 | assert performance.payload_mass == pytest.approx(7578, abs=1.)
60 | assert performance.delta_structural == pytest.approx(-27686, abs=1)
61 | assert performance.delta_payload == pytest.approx(-0.923, abs=1e-3)
62 |
63 |
64 | @check_dependency()
65 | def test_3_stages():
66 | rocket = Rocket(
67 | stages=[
68 | Stage(
69 | engines=[Engine.SRB],
70 | length=32.76,
71 | ),
72 | Stage(
73 | engines=[Engine.S_IVB],
74 | length=22.39,
75 | ),
76 | Stage(
77 | engines=[Engine.RS68],
78 | length=22.53,
79 | ),
80 | ],
81 | head_shape=HeadShape.ELLIPTICAL,
82 | ellipse_l_ratio=.175,
83 | length_diameter_ratio=17.55,
84 | max_q=50e3,
85 | payload_density=2810.
86 | )
87 |
88 | performance = RocketEvaluator.evaluate(rocket)
89 | assert performance.cost == pytest.approx(337894901.)
90 | assert performance.payload_mass == pytest.approx(57777, abs=1)
91 | assert performance.delta_structural == pytest.approx(-27031, abs=1)
92 | assert performance.delta_payload == pytest.approx(-118.9, abs=.1)
93 |
94 |
95 | @check_dependency()
96 | def test_rocket_problem():
97 | rocket = RocketArch()
98 | run_test_hierarchy(rocket, 2.83)
99 |
100 |
101 | @check_dependency()
102 | def test_lc_rocket_problem():
103 | rocket = LCRocketArch()
104 | run_test_hierarchy(rocket, 2.83)
105 |
106 |
107 | @check_dependency()
108 | def test_so_lc_rocket_problem():
109 | rocket = SOLCRocketArch(obj=RocketObj.OBJ_WEIGHTED)
110 | run_test_hierarchy(rocket, 2.83)
111 |
--------------------------------------------------------------------------------
/sb_arch_opt/problems/continuous.py:
--------------------------------------------------------------------------------
1 | """
2 | Licensed under the GNU General Public License, Version 3.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | https://www.gnu.org/licenses/gpl-3.0.html.en
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 |
14 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
15 | Contact: jasper.bussemaker@dlr.de
16 |
17 | This test suite contains a set of continuous unconstrained single-objective problems.
18 | """
19 | import numpy as np
20 | from pymoo.core.variable import Real
21 | from pymoo.problems.single.himmelblau import Himmelblau as HB
22 | from pymoo.problems.single.rosenbrock import Rosenbrock as RB
23 | from pymoo.problems.single.griewank import Griewank as GW
24 | from sb_arch_opt.problems.problems_base import *
25 |
26 | __all__ = ['Himmelblau', 'Rosenbrock', 'Griewank', 'Goldstein', 'Branin']
27 |
28 |
29 | class Himmelblau(NoHierarchyWrappedProblem):
30 |
31 | def __init__(self):
32 | super().__init__(HB())
33 |
34 |
35 | class Rosenbrock(NoHierarchyWrappedProblem):
36 |
37 | def __init__(self, n_var=10):
38 | super().__init__(RB(n_var=n_var))
39 |
40 |
41 | class Griewank(NoHierarchyWrappedProblem):
42 |
43 | def __init__(self):
44 | super().__init__(GW(n_var=10))
45 |
46 |
47 | class Goldstein(NoHierarchyProblemBase):
48 | """Goldstein-Price test problem, implementation based on
49 | https://github.com/scipy/scipy/blob/main/benchmarks/benchmarks/go_benchmark_functions/go_funcs_G.py#L88"""
50 |
51 | def __init__(self):
52 | des_vars = [Real(bounds=(-2, 2)) for _ in range(2)]
53 | super().__init__(des_vars)
54 |
55 | def _arch_evaluate(self, x: np.ndarray, is_active_out: np.ndarray, f_out: np.ndarray, g_out: np.ndarray,
56 | h_out: np.ndarray, *args, **kwargs):
57 | a = (1 + (x[:, 0] + x[:, 1] + 1) ** 2
58 | * (19 - 14 * x[:, 0] + 3 * x[:, 0] ** 2
59 | - 14 * x[:, 1] + 6 * x[:, 0] * x[:, 1] + 3 * x[:, 1] ** 2))
60 | b = (30 + (2 * x[:, 0] - 3 * x[:, 1]) ** 2
61 | * (18 - 32 * x[:, 0] + 12 * x[:, 0] ** 2
62 | + 48 * x[:, 1] - 36 * x[:, 0] * x[:, 1] + 27 * x[:, 1] ** 2))
63 | f_out[:, 0] = a*b
64 |
65 |
66 | class Branin(NoHierarchyProblemBase):
67 | """
68 | Branin test function from:
69 | Forrester, A., Sobester, A., & Keane, A. (2008). Engineering design via surrogate modelling: a practical guide.
70 | """
71 |
72 | _des_vars = [
73 | Real(bounds=(0, 1)), Real(bounds=(0, 1)),
74 | ]
75 |
76 | def __init__(self):
77 | super().__init__(self._des_vars)
78 |
79 | def _arch_evaluate(self, x: np.ndarray, is_active_out: np.ndarray, f_out: np.ndarray, g_out: np.ndarray,
80 | h_out: np.ndarray, *args, **kwargs):
81 |
82 | for i in range(x.shape[0]):
83 | f_out[i, 0] = self._h(x[i, 0], x[i, 1])
84 |
85 | @staticmethod
86 | def _h(x1, x2):
87 | t1 = (15*x2 - (5/(4*np.pi**2))*(15*x1-5)**2 + (5/np.pi)*(15*x1-5) - 6)**2
88 | t2 = 10*(1-1/(8*np.pi))*np.cos(15*x1-5) + 10
89 | return ((t1+t2)-54.8104)/51.9496
90 |
91 |
92 | if __name__ == '__main__':
93 | Himmelblau().print_stats()
94 | Rosenbrock().print_stats()
95 | Griewank().print_stats()
96 | Goldstein().print_stats()
97 | Branin().print_stats()
98 | # Branin().plot_design_space()
99 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | import os
26 | import sys
27 | sys.path.insert(0, os.path.dirname(__file__))
28 | from sb_arch_opt import __version__
29 | from setuptools import setup, find_packages
30 |
31 |
32 | def _get_readme():
33 | with open(os.path.join(os.path.dirname(__file__), 'README.md'), 'r') as fp:
34 | return fp.read()
35 |
36 |
37 | if __name__ == '__main__':
38 | setup(
39 | name='sb-arch-opt',
40 | version=__version__,
41 | description='SBArchOpt: Surrogate-Based Architecture Optimization',
42 | long_description=_get_readme(),
43 | long_description_content_type='text/markdown',
44 | author='Jasper Bussemaker',
45 | author_email='jasper.bussemaker@dlr.de',
46 | classifiers=[
47 | 'Intended Audience :: Science/Research',
48 | 'Topic :: Scientific/Engineering',
49 | 'Programming Language :: Python :: 3',
50 | 'License :: OSI Approved :: MIT License',
51 | ],
52 | license='MIT',
53 | install_requires=[
54 | 'numpy<2.0',
55 | 'pymoo~=0.6.1',
56 | 'scipy',
57 | 'deprecated',
58 | 'pandas',
59 | 'cached-property~=1.5',
60 | 'ConfigSpace~=1.2.1',
61 | 'more-itertools~=9.1',
62 | 'appdirs',
63 | ],
64 | extras_require={
65 | 'arch_sbo': [
66 | 'smt~=2.2,!=2.4,!=2.10.0',
67 | 'jenn~=1.0', # Until bug has been fixed: https://github.com/SMTorg/smt/issues/769
68 | 'numba',
69 | 'scikit-learn',
70 | ],
71 | # 'ota': [ # pip install -r requirements-ota.txt
72 | # 'open_turb_arch @ git+https://github.com/jbussemaker/OpenTurbofanArchitecting@pymoo_optional#egg=open_turb_arch',
73 | # ],
74 | # 'assignment': [ # pip install -r requirements-assignment.txt
75 | # 'assign_enc @ git+https://github.com/jbussemaker/AssignmentEncoding#egg=assign_enc',
76 | # ],
77 | 'botorch': [
78 | 'ax-platform~=0.3.0',
79 | 'botorch~=0.8.2',
80 | ],
81 | 'trieste': [
82 | 'trieste~=4.5',
83 | ],
84 | # 'tpe': [ # Not compatible with newer ConfigSpace
85 | # 'tpe==0.0.8',
86 | # ],
87 | 'hebo': [
88 | 'HEBO',
89 | ],
90 | 'rocket': [
91 | 'ambiance',
92 | ],
93 | 'egor': [
94 | 'egobox~=0.14.0',
95 | ],
96 | },
97 | python_requires='>=3.7',
98 | packages=find_packages(include='sb_arch_opt*'),
99 | )
100 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import itertools
3 | import numpy as np
4 | from typing import Optional, Tuple
5 | from sb_arch_opt.sampling import *
6 | from sb_arch_opt.problems.problems_base import *
7 | from pymoo.core.variable import Real, Integer, Choice
8 | from pymoo.problems.multi.zdt import ZDT1
9 |
10 |
11 | class DummyProblem(ArchOptTestProblemBase):
12 |
13 | def __init__(self, only_discrete=False, fail=False):
14 | self._problem = problem = ZDT1(n_var=2 if only_discrete else 5)
15 | if only_discrete:
16 | des_vars = [Choice(options=[str(9-j) for j in range(10)]) if i == 0 else Integer(bounds=(1, 10))
17 | for i in range(problem.n_var)]
18 | else:
19 | des_vars = [Real(bounds=(0, 1)) if i % 2 == 0 else (
20 | Choice(options=[str(9-j) for j in range(10)]) if i == 1 else Integer(bounds=(0, 9)))
21 | for i in range(problem.n_var)]
22 | self.only_discrete = only_discrete
23 | self.fail = fail
24 | self._provide_all_x = True
25 | self._i_eval = 0
26 | super().__init__(des_vars, n_obj=problem.n_obj)
27 |
28 | def might_have_hidden_constraints(self):
29 | return self.fail
30 |
31 | def _get_n_valid_discrete(self) -> int:
32 | if self.only_discrete:
33 | return 10*5 + 5
34 | return 10*10
35 |
36 | def set_provide_all_x(self, provide_all_x):
37 | self._provide_all_x = provide_all_x
38 | if 'all_discrete_x' in self.design_space.__dict__:
39 | del self.design_space.__dict__['all_discrete_x']
40 |
41 | def _gen_all_discrete_x(self) -> Optional[Tuple[np.ndarray, np.ndarray]]:
42 | if not self._provide_all_x:
43 | return
44 | x, is_active = [], []
45 | cartesian_prod_values = HierarchicalExhaustiveSampling.get_exhaustive_sample_values(self, n_cont=1)
46 | if self.only_discrete:
47 | for x_dv in itertools.product(*cartesian_prod_values):
48 | if x_dv[0] >= 5 and x_dv[1] != cartesian_prod_values[1][0]:
49 | continue
50 | x.append(x_dv)
51 | is_active.append([True, x_dv[0] < 5])
52 | else:
53 | for x_dv in itertools.product(*cartesian_prod_values):
54 | x.append(x_dv)
55 | is_active.append([True]*4+[x_dv[1] < 5])
56 | return np.array(x), np.array(is_active)
57 |
58 | def _arch_evaluate(self, x: np.ndarray, is_active_out: np.ndarray, f_out: np.ndarray, g_out: np.ndarray,
59 | h_out: np.ndarray, *args, **kwargs):
60 | self._correct_x_impute(x, is_active_out)
61 | assert np.all(x >= self.xl)
62 | assert np.all(x <= self.xu)
63 |
64 | i_dv = np.where(self.is_cat_mask)[0][0]
65 | cat_values = self.get_categorical_values(x, i_dv)
66 | assert np.all(x[:, i_dv] == [9-int(val) for val in cat_values])
67 | assert np.all((x[:, i_dv] == 0) == (cat_values == '9'))
68 |
69 | x_eval = x.copy()
70 | x_eval[:, self.is_discrete_mask] = (x_eval[:, self.is_discrete_mask]-self.xl[self.is_discrete_mask])/9
71 | out = self._problem.evaluate(x_eval, return_as_dictionary=True)
72 | f_out[:, :] = out['F']
73 |
74 | if self.fail:
75 | is_failed = np.zeros((len(x),), dtype=bool)
76 | is_failed[(self._i_eval % 2)::2] = True
77 | f_out[is_failed, :] = np.nan
78 | self._i_eval += len(x)
79 |
80 | def _correct_x(self, x: np.ndarray, is_active: np.ndarray):
81 | values = x[:, 0 if self.only_discrete else 1]
82 | is_active[:, -1] = values < 5
83 |
84 | def __repr__(self):
85 | return f'{self.__class__.__name__}(only_discrete={self.only_discrete})'
86 |
87 |
88 | @pytest.fixture
89 | def problem():
90 | return DummyProblem()
91 |
92 |
93 | @pytest.fixture
94 | def discrete_problem():
95 | return DummyProblem(only_discrete=True)
96 |
97 |
98 | @pytest.fixture
99 | def failing_problem():
100 | return DummyProblem(fail=True)
101 |
102 |
103 | def pytest_sessionstart(session):
104 | from sb_arch_opt.util import _prevent_capture
105 | print('PREVENT CAPTURE')
106 | _prevent_capture()
107 |
--------------------------------------------------------------------------------
/docs/algo/segomoe.md:
--------------------------------------------------------------------------------
1 | # SEGOMOE: Super Efficient Global Optimization with Mixture of Experts
2 |
3 | SEGOMOE is a Bayesian optimization toolbox developed by ONERA and ISAE-SUPAERO. For more information refer to:
4 |
5 | Bartoli, N., Lefebvre, T., Dubreuil, S., Olivanti, R., Priem, R., Bons, N., Martins J.R.R.A & Morlier, J. (2019).
6 | Adaptive modeling strategy for constrained global optimization with application to aerodynamic wing design. Aerospace
7 | Science and Technology, (90) 85-102.
8 |
9 | Priem, R., Bartoli, N., Diouane, Y., & Sgueglia, A. (2020). Upper trust bound feasibility criterion for mixed
10 | constrained Bayesian optimization with application to aircraft design. Aerospace Science and Technology, 105980.
11 |
12 | Saves, P., Bartoli, N., Diouane, Y., Lefebvre, T., Morlier, J., David, C., ... & Defoort, S. (2021, July). Constrained
13 | Bayesian optimization over mixed categorical variables, with application to aircraft design. In Proceedings of the
14 | International Conference on Multidisciplinary Design Optimization of Aerospace Systems (AEROBEST 2021) (pp. 1-758).
15 |
16 | ## Installation
17 |
18 | SEGOMOE is not openly available.
19 |
20 | ## Usage
21 |
22 | [API Reference](../api/segomoe.md)
23 |
24 | SEGOMOE is interacted with through the `SEGOMOEInterface` class. This class has a state containing evaluated (and
25 | failed) points, and requires a directory for results storage. The `run_optimization` function can be used to
26 | run the DOE and infill search.
27 |
28 | ```python
29 | from sb_arch_opt.algo.segomoe_interface import SEGOMOEInterface
30 |
31 | problem = ... # Subclass of ArchOptProblemBase
32 |
33 | # Define folder to store results in
34 | results_folder = ...
35 |
36 | # Use Mixture of Experts: automatically identifies clusters in the design space
37 | # with different best surrogates ("experts"). Can be more accurate, however
38 | # also greatly increases the cost of finding new infill points.
39 | use_moe = True
40 |
41 | # Options passed to the Sego class and to model generation, respectively
42 | sego_options = {}
43 | model_options = {}
44 |
45 | # Get the interface (will be initialized if the results folder has results)
46 | interface = SEGOMOEInterface(problem, results_folder, n_init=100, n_infill=50,
47 | use_moe=use_moe, sego_options=sego_options,
48 | model_options=model_options)
49 |
50 | # Initialize from other results if you want
51 | interface.initialize_from_previous('path/to/other/results_folder')
52 |
53 | # Run the optimization loop incl DOE
54 | interface.run_optimization()
55 |
56 | x = interface.x # (n, nx)
57 | x_failed = interface.x_failed # (n_failed, nx)
58 | f = interface.f # (n, nf)
59 | g = interface.g # (n, ng)
60 | pop = interface.pop # Population containing all design points
61 | opt = interface.opt # Population containing optimal point(s)
62 | ```
63 |
64 | ### pymoo API
65 |
66 | It is also possible to use the pymoo API to run an optimization:
67 | ```python
68 | from pymoo.optimize import minimize
69 | from sb_arch_opt.algo.segomoe_interface import SEGOMOEInterface, SEGOMOEAlgorithm
70 |
71 | problem = ... # Subclass of ArchOptProblemBase
72 |
73 | # Define folder to store results in
74 | results_folder = ...
75 |
76 | # Use Mixture of Experts: automatically identifies clusters in the design space
77 | # with different best surrogates ("experts"). Can be more accurate, however
78 | # also greatly increases the cost of finding new infill points.
79 | use_moe = True
80 |
81 | # Options passed to the Sego class and to model generation, respectively
82 | sego_options = {}
83 | model_options = {}
84 |
85 | # Get the interface (will be initialized if the results folder has results)
86 | interface = SEGOMOEInterface(problem, results_folder, n_init=100, n_infill=50,
87 | use_moe=use_moe, sego_options=sego_options,
88 | model_options=model_options)
89 |
90 | # Define the pymoo Algorithm
91 | algo = SEGOMOEAlgorithm(interface)
92 |
93 | # Initialize from other results if you want
94 | algo.initialize_from_previous_results(problem, results_folder='/optional/other/result/folder')
95 |
96 | # Run the optimization
97 | # Note: no need to give a termination, as that is already defined by the SEGOMOEInterface object (n_init + n_infill)
98 | result = minimize(problem, algo, seed=42) # Remove seed in production
99 | ```
100 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/arch_sbo/metrics.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | from pymoo.core.indicator import Indicator
26 | from pymoo.indicators.hv import Hypervolume
27 | from pymoo.util.display.column import Column
28 | from pymoo.core.termination import TerminateIfAny
29 | from pymoo.termination.max_gen import MaximumGenerationTermination
30 | from sb_arch_opt.algo.pymoo_interface.metrics import *
31 |
32 | __all__ = ['EstimatedPFDistance', 'get_sbo_termination', 'PFDistanceTermination', 'SBOMultiObjectiveOutput']
33 |
34 |
35 | def get_sbo_termination(n_max_infill: int, tol=1e-3, n_filter=2):
36 | return PFDistanceTermination(tol=tol, n_filter=n_filter, n_max_infill=n_max_infill)
37 |
38 |
39 | class EstimatedPFDistance(Indicator):
40 | """Indicates the distance between the current Pareto front and the one estimated by the underlying model"""
41 |
42 | def __init__(self):
43 | super().__init__()
44 | self.algorithm = None
45 |
46 | def _do(self, f, *args, **kwargs):
47 | if self.algorithm is None:
48 | raise RuntimeError('Algorithm not set!')
49 | from sb_arch_opt.algo.arch_sbo.algo import InfillAlgorithm, SBOInfill
50 |
51 | if len(f) == 0:
52 | return 1
53 |
54 | if isinstance(self.algorithm, InfillAlgorithm) and isinstance(self.algorithm.infill_obj, SBOInfill):
55 | sbo_infill = self.algorithm.infill_obj
56 | pf_estimate = sbo_infill.get_pf_estimate()
57 | if pf_estimate is None:
58 | return 1
59 |
60 | hv = Hypervolume(pf=pf_estimate)
61 | hv_estimate = hv.do(pf_estimate)
62 | hv_f = hv.do(f)
63 |
64 | hv_dist = 1 - (hv_f / hv_estimate)
65 | if hv_dist < 0:
66 | hv_dist = 0
67 | return hv_dist
68 |
69 | return 0
70 |
71 |
72 | class PFDistanceTermination(TerminateIfAny):
73 | """Termination criterion tracking the difference between the found and estimated Pareto fronts"""
74 |
75 | def __init__(self, tol=1e-3, n_filter=2, n_max_infill=100):
76 | self._pf_dist = EstimatedPFDistance()
77 | termination = [
78 | IndicatorDeltaToleranceTermination(SmoothedIndicator(self._pf_dist, n_filter=n_filter), tol),
79 | MaximumGenerationTermination(n_max_gen=n_max_infill),
80 | ]
81 | super().__init__(*termination)
82 |
83 | def update(self, algorithm):
84 | self._pf_dist.algorithm = algorithm
85 | return super().update(algorithm)
86 |
87 |
88 | class SBOMultiObjectiveOutput(EHVMultiObjectiveOutput):
89 | """Extended multi-objective output for use with SBO"""
90 |
91 | def __init__(self):
92 | super().__init__()
93 | self.pf_dist_col = Column('pf_dist')
94 | self.pf_dist = EstimatedPFDistance()
95 |
96 | def initialize(self, algorithm):
97 | super().initialize(algorithm)
98 | self.pf_dist.algorithm = algorithm
99 | self.columns += [self.pf_dist_col]
100 |
101 | def update(self, algorithm):
102 | super().update(algorithm)
103 |
104 | f, feas = algorithm.opt.get("F", "feas")
105 | f = f[feas]
106 |
107 | self.pf_dist_col.set(self.pf_dist.do(f) if len(f) > 0 else None)
108 |
--------------------------------------------------------------------------------
/docs/algo/pymoo.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # pymoo
4 |
5 | [pymoo](https://pymoo.org/) is a multi-objective optimization framework that supports mixed-discrete problem
6 | definitions. It includes many test problems and algorithms, mostly evolutionary algorithms such as a Genetic Algorithm
7 | and the Non-dominated Sorting Genetic Algorithm 2 (NSGA2).
8 |
9 | The architecture optimization problem base class is based on pymoo.
10 |
11 | ## Installation
12 |
13 | No further actions required.
14 |
15 | ## Usage
16 |
17 | [API Reference](../api/pymoo.md)
18 |
19 | Since the problem definition is based on pymoo, pymoo algorithms work out-of-the-box. However, their effectiveness can
20 | be improved by provisioning them with architecture optimization repair operators and repaired sampling strategies.
21 |
22 | ```python
23 | from pymoo.optimize import minimize
24 | from pymoo.algorithms.soo.nonconvex.ga import GA
25 | from sb_arch_opt.algo.pymoo_interface import provision_pymoo
26 |
27 | problem = ... # Subclass of ArchOptProblemBase
28 |
29 | ga_algorithm = GA(pop_size=100)
30 | provision_pymoo(ga_algorithm) # See intermediate results storage below
31 | result = minimize(problem, ga_algorithm, termination=('n_gen', 10), seed=42) # Remove seed when using in production!
32 | ```
33 |
34 | Or to simply get a ready-to-use NSGA2:
35 | ```python
36 | from sb_arch_opt.algo.pymoo_interface import get_nsga2
37 |
38 | nsga2 = get_nsga2(pop_size=100)
39 | ```
40 |
41 | ### Intermediate Results Storage and Restarting
42 |
43 | Storing intermediate results can be useful in case of a problem during optimization. Similarly, restarting can be useful
44 | for continuing a previously failed optimization, or for adding more generations to a previous optimization run.
45 |
46 | To enable intermediate results storage, provide a path to a folder where results can be stored to `provision_pymoo` or
47 | `get_nsga2`.
48 |
49 | To restart an optimization from a previous run, intermediate results storage must have been used in that previous run.
50 | To then initialize an algorithm, use the `initialize_from_previous_results` function. Partial results are stored after
51 | each evaluation (or after `problem.get_n_batch_evaluate()` evaluations), so even partially-evaluated populations can
52 | be recovered.
53 |
54 | ```python
55 | from pymoo.optimize import minimize
56 | from pymoo.algorithms.soo.nonconvex.ga import GA
57 | from sb_arch_opt.algo.pymoo_interface import provision_pymoo, \
58 | initialize_from_previous_results
59 |
60 | problem = ... # Subclass of ArchOptProblemBase
61 |
62 | results_folder_path = 'path/to/results/folder'
63 | ga_algorithm = GA(pop_size=100)
64 |
65 | # Enable intermediate results storage
66 | provision_pymoo(ga_algorithm, results_folder=results_folder_path)
67 |
68 | # Start from previous results (skipped if no previous results are available)
69 | initialize_from_previous_results(ga_algorithm, problem, results_folder_path)
70 |
71 | result = minimize(problem, ga_algorithm, termination=('n_gen', 10))
72 | ```
73 |
74 | For running large DOE's with intermediate results storage, you can use `get_doe_algo`:
75 |
76 | ```python
77 | from sb_arch_opt.algo.pymoo_interface import get_doe_algo, \
78 | load_from_previous_results
79 |
80 | problem = ... # Subclass of ArchOptProblemBase
81 | results_folder_path = 'path/to/results/folder'
82 |
83 | # Get DOE algorithm and run
84 | doe_algo = get_doe_algo(doe_size=100, results_folder=results_folder_path)
85 | doe_algo.setup(problem, seed=42) # Remove seed argument when using in production!
86 | doe_algo.run()
87 |
88 | # Evaluate the sampled points
89 | pop = doe_algo.pop
90 |
91 | # Load intermediate results in case of crash
92 | pop = load_from_previous_results(problem, results_folder_path)
93 | ```
94 |
95 | You can increase DOE algo's DOE size after a stored run:
96 | ```python
97 | from sb_arch_opt.algo.pymoo_interface import get_doe_algo, \
98 | initialize_from_previous_results
99 |
100 | problem = ... # Subclass of ArchOptProblemBase
101 | results_folder_path = 'path/to/results/folder'
102 |
103 | doe_algo = get_doe_algo(doe_size=200, results_folder=results_folder_path)
104 |
105 | # Initializing from previous results ignores the previously-set DOE size
106 | initialize_from_previous_results(doe_algo, problem, results_folder_path)
107 | doe_algo.set_doe_size(problem, doe_size=200)
108 |
109 | doe_algo.setup(problem, seed=42) # Remove seed argument when using in production!
110 | doe_algo.run()
111 |
112 | # Evaluate the sampled points
113 | pop = doe_algo.pop
114 | ```
115 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/arch_sbo/api.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | from sb_arch_opt.problem import *
26 | from sb_arch_opt.algo.arch_sbo.models import *
27 | from sb_arch_opt.algo.arch_sbo.algo import *
28 | from sb_arch_opt.algo.arch_sbo.infill import *
29 | from sb_arch_opt.algo.arch_sbo.metrics import *
30 | from sb_arch_opt.algo.arch_sbo.hc_strategy import *
31 |
32 | if not HAS_ARCH_SBO:
33 | def get_sbo_termination(*_, **__):
34 | return None
35 |
36 |
37 | __all__ = ['get_arch_sbo_rbf', 'get_arch_sbo_gp', 'HAS_ARCH_SBO', 'HAS_SMT', 'get_sbo_termination', 'get_sbo',
38 | 'ConstraintAggregation']
39 |
40 |
41 | def get_arch_sbo_rbf(init_size: int = 100, results_folder=None, **kwargs) -> InfillAlgorithm:
42 | """
43 | Get a architecture SBO algorithm using an RBF model as its surrogate model.
44 | """
45 | model = ModelFactory.get_rbf_model()
46 | hc_strategy = get_hc_strategy()
47 | return get_sbo(model, FunctionEstimateInfill(), init_size=init_size, results_folder=results_folder,
48 | hc_strategy=hc_strategy, **kwargs)
49 |
50 |
51 | def get_arch_sbo_gp(problem: ArchOptProblemBase, init_size: int = 100, n_parallel=None, min_pof: float = None,
52 | kpls_n_dim: int = 10, g_aggregation: ConstraintAggregation = None, results_folder=None, **kwargs) \
53 | -> InfillAlgorithm:
54 | """
55 | Get an architecture SBO algorithm using a mixed-discrete Gaussian Process (Kriging) model as its surrogate model.
56 | Appropriate (multi-objective) infills and constraint handling techniques are automatically selected.
57 |
58 | For constraint handling, increase min_pof to between 0.50 and 0.75 to be more conservative (i.e. require a higher
59 | probability of being feasible for infill points) or decrease below 0.50 to be more exploratory. Optionally defined
60 | an aggregation strategy to reduce the number of models to train.
61 |
62 | To reduce model training times for high-dimensional problems, KPLS is used instead of Kriging when the problem
63 | dimension exceeds kpls_n_dim. Note that the DoE should then contain at least kpls_n_dim+1 points.
64 | """
65 |
66 | # Create the mixed-discrete Kriging model, correctly configured for the given design space
67 | kpls_n_comp = kpls_n_dim if kpls_n_dim is not None and problem.n_var > kpls_n_dim else None
68 | model, normalization = ModelFactory(problem).get_md_kriging_model(kpls_n_comp=kpls_n_comp)
69 |
70 | # Select the single- or multi-objective infill criterion, including constraint handling strategy
71 | infill, infill_batch = get_default_infill(
72 | problem, n_parallel=n_parallel, min_pof=min_pof, g_aggregation=g_aggregation)
73 |
74 | # Get default hidden constraint strategy
75 | hc_strategy = get_hc_strategy(kpls_n_dim=kpls_n_dim)
76 |
77 | return get_sbo(model, infill, infill_size=infill_batch, init_size=init_size, normalization=normalization,
78 | results_folder=results_folder, hc_strategy=hc_strategy, **kwargs)
79 |
80 |
81 | def get_sbo(surrogate_model, infill: 'SurrogateInfill', infill_size: int = 1, init_size: int = 100,
82 | infill_pop_size: int = 100, infill_gens: int = None, repair=None, normalization=None,
83 | hc_strategy: 'HiddenConstraintStrategy' = None, results_folder=None, **kwargs) -> InfillAlgorithm:
84 | """Create the SBO algorithm given some SMT surrogate model and an infill criterion"""
85 |
86 | sbo = SBOInfill(surrogate_model, infill, pop_size=infill_pop_size, termination=infill_gens, repair=repair,
87 | normalization=normalization, hc_strategy=hc_strategy, verbose=True)\
88 | .algorithm(infill_size=infill_size, init_size=init_size, **kwargs)
89 |
90 | if results_folder is not None:
91 | sbo.store_intermediate_results(results_folder=results_folder)
92 | return sbo
93 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/test_hierarchical.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import numpy as np
4 | from sb_arch_opt.sampling import *
5 | from sb_arch_opt.problems.continuous import *
6 | from sb_arch_opt.problems.hierarchical import *
7 | from pymoo.core.evaluator import Evaluator
8 |
9 |
10 | def run_test_hierarchy(problem, imp_ratio, check_n_valid=True, validate_exhaustive=False, exh_n_cont=3,
11 | corr_ratio=None):
12 | x_discrete, is_act_discrete = problem.all_discrete_x
13 | if check_n_valid or x_discrete is not None:
14 | if x_discrete is not None:
15 | assert np.all(~LargeDuplicateElimination.eliminate(x_discrete))
16 | assert x_discrete.shape[0] == problem.get_n_valid_discrete()
17 |
18 | if validate_exhaustive:
19 | x_trail_repair, _ = HierarchicalExhaustiveSampling.get_all_x_discrete_by_trial_and_repair(problem)
20 | assert {tuple(ix) for ix in x_trail_repair} == {tuple(ix) for ix in x_discrete}
21 |
22 | is_cond_act = problem.is_conditionally_active
23 | assert np.all(is_cond_act == np.any(~is_act_discrete, axis=0))
24 |
25 | pop = HierarchicalExhaustiveSampling(n_cont=1).do(problem, 0)
26 | assert len(pop) == problem.get_n_valid_discrete()
27 |
28 | x_discrete, is_act_discrete = problem.all_discrete_x
29 | assert HierarchicalExhaustiveSampling.has_cheap_all_x_discrete(problem) == (x_discrete is not None)
30 |
31 | assert problem.get_discrete_imputation_ratio() == pytest.approx(imp_ratio, rel=.02)
32 | if corr_ratio is None:
33 | corr_ratio = 1 # Assume no correction
34 | if np.isnan(corr_ratio):
35 | assert np.isnan(problem.get_discrete_correction_ratio())
36 | else:
37 | assert problem.get_discrete_correction_ratio() == pytest.approx(corr_ratio, rel=.02)
38 | assert problem.get_discrete_correction_ratio() <= problem.get_discrete_imputation_ratio()
39 | problem.print_stats()
40 |
41 | pop = None
42 | if exh_n_cont != -1 and HierarchicalExhaustiveSampling.get_n_sample_exhaustive(problem, n_cont=exh_n_cont) < 1e3:
43 | try:
44 | pop = HierarchicalExhaustiveSampling(n_cont=exh_n_cont).do(problem, 0)
45 | except MemoryError:
46 | pass
47 | if pop is None:
48 | pop = HierarchicalSampling().do(problem, 100)
49 | Evaluator().eval(problem, pop)
50 | problem.get_population_statistics(pop, show=True)
51 | return pop
52 |
53 |
54 | def test_hier_goldstein():
55 | run_test_hierarchy(HierarchicalGoldstein(), 2.25)
56 |
57 |
58 | def test_mo_hier_goldstein():
59 | run_test_hierarchy(MOHierarchicalGoldstein(), 2.25)
60 |
61 |
62 | def test_hier_rosenbrock():
63 | run_test_hierarchy(HierarchicalRosenbrock(), 1.5)
64 |
65 |
66 | def test_mo_hier_rosenbrock():
67 | run_test_hierarchy(MOHierarchicalRosenbrock(), 1.5)
68 |
69 |
70 | def test_hier_zaefferer():
71 | run_test_hierarchy(ZaeffererHierarchical.from_mode(ZaeffererProblemMode.A_OPT_INACT_IMP_PROF_UNI), 1)
72 |
73 | problem = ZaeffererHierarchical.from_mode(ZaeffererProblemMode.A_OPT_INACT_IMP_PROF_UNI)
74 | x, is_act = problem.correct_x(np.array([[0, .75]]))
75 | assert np.all(x == [[0, .5]])
76 | assert np.all(is_act == [[True, False]])
77 |
78 | assert problem.get_imputation_ratio() == 2/(2-problem.c)
79 | assert problem.get_correction_ratio() == 1
80 |
81 |
82 | def test_hier_test_problem():
83 | run_test_hierarchy(MOHierarchicalTestProblem(), 72)
84 |
85 |
86 | def test_jenatton():
87 | problem = Jenatton()
88 | run_test_hierarchy(problem, 2)
89 | run_test_hierarchy(problem, 2)
90 |
91 | run_test_hierarchy(Jenatton(explicit=False), 2)
92 |
93 |
94 | def test_hier_branin():
95 | run_test_hierarchy(HierBranin(), 3.24, validate_exhaustive=True, corr_ratio=1.05)
96 |
97 |
98 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
99 | def test_hier_zdt1():
100 | run_test_hierarchy(HierZDT1Small(), 1.8, validate_exhaustive=True, corr_ratio=1.125)
101 | run_test_hierarchy(HierZDT1(), 4.86, validate_exhaustive=True, corr_ratio=1.07)
102 | run_test_hierarchy(HierZDT1Large(), 8.19, corr_ratio=1.12)
103 | run_test_hierarchy(HierDiscreteZDT1(), 4.1, corr_ratio=1.12)
104 |
105 |
106 | def test_hier_cantilevered_beam():
107 | run_test_hierarchy(HierCantileveredBeam(), 5.4, corr_ratio=1.04)
108 |
109 |
110 | def test_hier_carside():
111 | run_test_hierarchy(HierCarside(), 6.48, corr_ratio=1.05)
112 |
113 |
114 | def test_hier_nn():
115 | run_test_hierarchy(NeuralNetwork(), 2.51)
116 |
117 |
118 | def test_tunable_hierarchical_meta_problem():
119 | prob1 = TunableHierarchicalMetaProblem(lambda n: Branin(), imp_ratio=10, n_subproblem=4, diversity_range=.5)
120 | prob1.print_stats()
121 |
122 | prob2 = TunableHierarchicalMetaProblem(lambda n: Branin(), imp_ratio=10, n_subproblem=5, diversity_range=.25)
123 | prob2.print_stats()
124 | assert prob1._pf_cache_path() != prob2._pf_cache_path()
125 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # SBArchOpt: Surrogate-Based Architecture Optimization
4 |
5 | [](https://github.com/jbussemaker/SBArchOpt/actions/workflows/tests.yml?query=workflow%3ATests)
6 | [](https://pypi.org/project/sb-arch-opt)
7 | [](LICENSE)
8 | [](https://joss.theoj.org/papers/0b2b765c04d31a4cead77140f82ecba0)
9 | [](https://sbarchopt.readthedocs.io/en/latest/?badge=latest)
10 |
11 | [GitHub Repository](https://github.com/jbussemaker/SBArchOpt) |
12 | [Documentation](https://sbarchopt.readthedocs.io/)
13 |
14 | SBArchOpt (es-bee-ARK-opt) provides a set of classes and interfaces for applying Surrogate-Based Optimization (SBO)
15 | for system architecture optimization problems:
16 | - Expensive black-box problems: evaluating one candidate architecture might be computationally expensive
17 | - Mixed-discrete design variables: categorical architectural decisions mixed with continuous sizing variables
18 | - Hierarchical design variables: decisions can deactivate/activate (parts of) downstream decisions
19 | - Multi-objective: stemming from conflicting stakeholder needs
20 | - Subject to hidden constraints: simulation tools might not converge for all design points
21 |
22 | Surrogate-Based Optimization (SBO) aims to accelerate convergence by fitting a surrogate model
23 | (e.g. regression, gaussian process, neural net) to the inputs (design variables) and outputs (objectives/constraints)
24 | to try to predict where interesting infill points lie. Potentially, SBO needs about one or two orders of magnitude less
25 | function evaluations than Multi-Objective Evolutionary Algorithms (MOEA's) like NSGA2. However, dealing with the
26 | specific challenges of architecture optimization, especially in a combination of the challenges, is not trivial.
27 | This library hopes to support in doing this.
28 |
29 | The library provides:
30 | - A common interface for defining architecture optimization problems based on [pymoo](https://pymoo.org/)
31 | - Support in using Surrogate-Based Optimization (SBO) algorithms:
32 | - Implementation of a basic SBO algorithm
33 | - Connectors to various external SBO libraries
34 | - Analytical and realistic test problems that exhibit one or more of the architecture optimization challenges
35 |
36 | ## Installation
37 |
38 | First, create a conda environment (skip if you already have one):
39 | ```
40 | conda create --name opt python=3.11
41 | conda activate opt
42 | ```
43 |
44 | Then install the package:
45 | ```
46 | conda install "numpy<2.0"
47 | pip install sb-arch-opt
48 | ```
49 |
50 | Note: there are optional dependencies for the connected optimization frameworks and test problems.
51 | Refer to their documentation for dedicated installation instructions.
52 |
53 | ## Documentation
54 |
55 | Refer to the [documentation](https://sbarchopt.readthedocs.io/) for more background on SBArchOpt
56 | and how to implement architecture optimization problems.
57 |
58 | ## Citing
59 |
60 | If you use SBArchOpt in your work, please cite it:
61 |
62 | Bussemaker, J.H., (2023). SBArchOpt: Surrogate-Based Architecture Optimization. Journal of Open Source Software, 8(89),
63 | 5564, DOI: [10.21105/joss.05564](https://doi.org/10.21105/joss.05564)
64 |
65 | Bussemaker, J.H., et al., (2025). System Architecture Optimization Strategies: Dealing with Expensive Hierarchical
66 | Problems. Journal of Global Optimization, 91(4), 851-895.
67 | DOI: [10.1007/s10898-024-01443-8](https://link.springer.com/article/10.1007/s10898-024-01443-8)
68 |
69 | Bussemaker, J.H., et al., (2024). Surrogate-Based Optimization of System Architectures Subject to Hidden Constraints.
70 | In AIAA AVIATION 2024 FORUM. Las Vegas, NV, USA.
71 | DOI: [10.2514/6.2024-4401](https://arc.aiaa.org/doi/10.2514/6.2024-4401)
72 |
73 | ## Contributing
74 |
75 | The project is coordinated by: Jasper Bussemaker (*jasper.bussemaker at dlr.de*)
76 |
77 | If you find a bug or have a feature request, please file an issue using the Github issue tracker.
78 | If you require support for using SBArchOpt or want to collaborate, feel free to contact me.
79 |
80 | Contributions are appreciated too:
81 | - Fork the repository
82 | - Add your contributions to the fork
83 | - Update/add documentation
84 | - Add tests and make sure they pass (tests are run using `pytest`)
85 | - Read and sign the [Contributor License Agreement (CLA)](https://github.com/jbussemaker/SBArchOpt/blob/main/SBArchOpt%20DLR%20Individual%20Contributor%20License%20Agreement.docx)
86 | , and send it to the project coordinator
87 | - Issue a pull request into the `dev` branch
88 |
89 | ### Adding Documentation
90 |
91 | ```
92 | pip install -r requirements-docs.txt
93 | mkdocs serve
94 | ```
95 |
96 | Refer to [mkdocs](https://www.mkdocs.org/) and [mkdocstrings](https://mkdocstrings.github.io/) documentation
97 | for more information.
98 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/windows,pycharm,python,jupyternotebooks
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,pycharm,python,jupyternotebooks
3 |
4 | ### JupyterNotebooks ###
5 | # gitignore template for Jupyter Notebooks
6 | # website: http://jupyter.org/
7 |
8 | .ipynb_checkpoints
9 | */.ipynb_checkpoints/*
10 |
11 | # IPython
12 | profile_default/
13 | ipython_config.py
14 |
15 | # Remove previous ipynb_checkpoints
16 | # git rm -r .ipynb_checkpoints/
17 |
18 | ### JetBrains ###
19 | .idea
20 |
21 | ### Python ###
22 | # Byte-compiled / optimized / DLL files
23 | __pycache__/
24 | *.py[cod]
25 | *$py.class
26 |
27 | # C extensions
28 | *.so
29 |
30 | # Distribution / packaging
31 | .Python
32 | build/
33 | develop-eggs/
34 | dist/
35 | downloads/
36 | eggs/
37 | .eggs/
38 | lib/
39 | lib64/
40 | parts/
41 | sdist/
42 | var/
43 | wheels/
44 | share/python-wheels/
45 | *.egg-info/
46 | .installed.cfg
47 | *.egg
48 | MANIFEST
49 |
50 | # PyInstaller
51 | # Usually these files are written by a python script from a template
52 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
53 | *.manifest
54 | *.spec
55 |
56 | # Installer logs
57 | pip-log.txt
58 | pip-delete-this-directory.txt
59 |
60 | # Unit test / coverage reports
61 | htmlcov/
62 | .tox/
63 | .nox/
64 | .coverage
65 | .coverage.*
66 | .cache
67 | nosetests.xml
68 | coverage.xml
69 | *.cover
70 | *.py,cover
71 | .hypothesis/
72 | .pytest_cache/
73 | cover/
74 |
75 | # Translations
76 | *.mo
77 | *.pot
78 |
79 | # Django stuff:
80 | *.log
81 | local_settings.py
82 | db.sqlite3
83 | db.sqlite3-journal
84 |
85 | # Flask stuff:
86 | instance/
87 | .webassets-cache
88 |
89 | # Scrapy stuff:
90 | .scrapy
91 |
92 | # Sphinx documentation
93 | docs/_build/
94 |
95 | # PyBuilder
96 | .pybuilder/
97 | target/
98 |
99 | # Jupyter Notebook
100 |
101 | # IPython
102 |
103 | # pyenv
104 | # For a library or package, you might want to ignore these files since the code is
105 | # intended to run in multiple environments; otherwise, check them in:
106 | # .python-version
107 |
108 | # pipenv
109 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
110 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
111 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
112 | # install all needed dependencies.
113 | #Pipfile.lock
114 |
115 | # poetry
116 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
117 | # This is especially recommended for binary packages to ensure reproducibility, and is more
118 | # commonly ignored for libraries.
119 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
120 | #poetry.lock
121 |
122 | # pdm
123 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
124 | #pdm.lock
125 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
126 | # in version control.
127 | # https://pdm.fming.dev/#use-with-ide
128 | .pdm.toml
129 |
130 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
131 | __pypackages__/
132 |
133 | # Celery stuff
134 | celerybeat-schedule
135 | celerybeat.pid
136 |
137 | # SageMath parsed files
138 | *.sage.py
139 |
140 | # Environments
141 | .env
142 | .venv
143 | env/
144 | venv/
145 | ENV/
146 | env.bak/
147 | venv.bak/
148 |
149 | # Spyder project settings
150 | .spyderproject
151 | .spyproject
152 |
153 | # Rope project settings
154 | .ropeproject
155 |
156 | # mkdocs documentation
157 | /site
158 |
159 | # mypy
160 | .mypy_cache/
161 | .dmypy.json
162 | dmypy.json
163 |
164 | # Pyre type checker
165 | .pyre/
166 |
167 | # pytype static type analyzer
168 | .pytype/
169 |
170 | # Cython debug symbols
171 | cython_debug/
172 |
173 | # PyCharm
174 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
175 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
176 | # and can be added to the global gitignore or merged into this file. For a more nuclear
177 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
178 | #.idea/
179 |
180 | ### Python Patch ###
181 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
182 | poetry.toml
183 |
184 | # ruff
185 | .ruff_cache/
186 |
187 | ### Windows ###
188 | # Windows thumbnail cache files
189 | Thumbs.db
190 | Thumbs.db:encryptable
191 | ehthumbs.db
192 | ehthumbs_vista.db
193 |
194 | # Dump file
195 | *.stackdump
196 |
197 | # Folder config file
198 | [Dd]esktop.ini
199 |
200 | # Recycle Bin used on file shares
201 | $RECYCLE.BIN/
202 |
203 | # Windows Installer files
204 | *.cab
205 | *.msi
206 | *.msix
207 | *.msm
208 | *.msp
209 |
210 | # Windows shortcuts
211 | *.lnk
212 |
213 | # End of https://www.toptal.com/developers/gitignore/api/windows,pycharm,python,jupyternotebooks
214 |
215 | n2.html
216 | site/
217 |
218 | sb_arch_opt/problems/turbofan_data
219 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/segomoe_interface/pymoo_algo.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2024, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | import logging
26 | from sb_arch_opt.problem import *
27 | from sb_arch_opt.algo.pymoo_interface import *
28 | from sb_arch_opt.algo.pymoo_interface.metrics import EHVMultiObjectiveOutput
29 |
30 | from pymoo.core.algorithm import Algorithm
31 | from pymoo.core.population import Population
32 | from pymoo.util.optimum import filter_optimum
33 | from pymoo.termination.max_eval import MaximumFunctionCallTermination
34 |
35 | from sb_arch_opt.algo.segomoe_interface.algo import SEGOMOEInterface, check_dependencies
36 |
37 | __all__ = ['SEGOMOEAlgorithm']
38 |
39 | log = logging.getLogger('sb_arch_opt.segomoe')
40 |
41 |
42 | class SEGOMOEAlgorithm(Algorithm):
43 | """
44 | Algorithm that wraps the SEGOMOE interface.
45 |
46 | The population state is managed here, and each time infill points are asked for the SEGOMOE population is updated
47 | from the algorithm population.
48 | """
49 |
50 | def __init__(self, segomoe: SEGOMOEInterface, output=EHVMultiObjectiveOutput(), **kwargs):
51 | check_dependencies()
52 | super().__init__(output=output, **kwargs)
53 | self.segomoe = segomoe
54 |
55 | self.termination = MaximumFunctionCallTermination(self.segomoe.n_init + self.segomoe.n_infill)
56 | self._store_intermediate_results()
57 |
58 | self.initialization = None # Enable DOE override
59 |
60 | def _initialize_infill(self):
61 | if self.initialization is not None:
62 | return self.initialization.do(self.problem, self.segomoe.n_init, algorithm=self)
63 | return self._infill()
64 |
65 | def _initialize_advance(self, infills=None, **kwargs):
66 | self._advance(infills=infills, **kwargs)
67 |
68 | def has_next(self):
69 | if not super().has_next():
70 | return False
71 |
72 | self._infill_set_pop()
73 | if not self.segomoe.optimization_has_ask():
74 | return False
75 | return True
76 |
77 | def _infill(self):
78 | self._infill_set_pop()
79 | x_infill = self.segomoe.optimization_ask()
80 | off = Population.new(X=x_infill) if x_infill is not None else Population.new()
81 |
82 | # Stop if no new offspring is generated
83 | if len(off) == 0:
84 | self.termination.force_termination = True
85 |
86 | return off
87 |
88 | def _infill_set_pop(self):
89 | if self.pop is None or len(self.pop) == 0:
90 | self.segomoe.set_pop(pop=None)
91 | else:
92 | self.segomoe.set_pop(self.pop)
93 |
94 | def _advance(self, infills=None, **kwargs):
95 | if infills is not None:
96 | self.segomoe.optimization_tell_pop(infills)
97 | self.pop = self.segomoe.pop
98 |
99 | def _set_optimum(self):
100 | pop = self.pop
101 | i_failed = ArchOptProblemBase.get_failed_points(pop)
102 | valid_pop = pop[~i_failed]
103 | if len(valid_pop) == 0:
104 | self.opt = Population.new(X=[None])
105 | else:
106 | self.opt = filter_optimum(valid_pop, least_infeasible=True)
107 |
108 | def _store_intermediate_results(self):
109 | """Enable intermediate results storage to support restarting"""
110 | results_folder = self.segomoe.results_folder
111 | self.evaluator = ArchOptEvaluator(results_folder=results_folder)
112 | self.callback = ResultsStorageCallback(results_folder, callback=self.callback)
113 |
114 | def initialize_from_previous_results(self, problem: ArchOptProblemBase, results_folder: str = None) -> bool:
115 | """Initialize the SBO algorithm from previously stored results"""
116 | if results_folder is None:
117 | results_folder = self.segomoe.results_folder
118 |
119 | population = load_from_previous_results(problem, results_folder)
120 | if population is None:
121 | return False
122 |
123 | self.pop = population
124 | self._set_optimum()
125 | return True
126 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/smarty_interface/algo.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | import logging
26 | import numpy as np
27 | from pymoo.core.population import Population
28 | from sb_arch_opt.problem import ArchOptProblemBase
29 |
30 | try:
31 | from smarty.problem.optimizationProblem import CustomOptProb
32 | from smarty.optimize.sbo import SBO
33 | from smarty.optimize import convergenceCriteria as CC
34 | from smarty import Log
35 |
36 | HAS_SMARTY = True
37 | except ImportError:
38 | HAS_SMARTY = False
39 |
40 | __all__ = ['HAS_SMARTY', 'check_dependencies', 'SMARTyArchOptInterface']
41 |
42 | log = logging.getLogger('sb_arch_opt.smarty')
43 |
44 |
45 | def check_dependencies():
46 | if not HAS_SMARTY:
47 | raise ImportError('SMARTy not installed!')
48 |
49 |
50 | class SMARTyArchOptInterface:
51 | """
52 | Interface class to SMARTy SBO.
53 | """
54 |
55 | def __init__(self, problem: ArchOptProblemBase, n_init: int, n_infill: int):
56 | check_dependencies()
57 | Log.SetLogLevel(1)
58 |
59 | self._problem = problem
60 | self._n_init = n_init
61 | self._n_infill = n_infill
62 | self._has_g = problem.n_ieq_constr > 0
63 |
64 | self._opt_prob = None
65 | self._optimizer = None
66 |
67 | @property
68 | def problem(self):
69 | return self._problem
70 |
71 | @property
72 | def opt_prob(self):
73 | if self._opt_prob is None:
74 | bounds = np.column_stack([self._problem.xl, self._problem.xu])
75 |
76 | problem_structure = {'objFuncs': {f'f{i}': 'F' for i in range(self._problem.n_obj)}}
77 | if self._has_g:
78 | problem_structure['constrFuncs'] = {f'g{i}': 'F' for i in range(self._problem.n_ieq_constr)}
79 |
80 | self._opt_prob = CustomOptProb(bounds=bounds, problemStructure=problem_structure,
81 | customFunctionHandler=self._evaluate, vectorized=True,
82 | problemName=repr(self._problem))
83 | return self._opt_prob
84 |
85 | @property
86 | def optimizer(self) -> 'SBO':
87 | if self._optimizer is None:
88 | self._optimizer = sbo = SBO(self.opt_prob)
89 |
90 | for key, settings in sbo._settingsDOE.items():
91 | settings['nSamples'] = self._n_init
92 |
93 | return self._optimizer
94 |
95 | def _evaluate(self, x, _):
96 | out = self._problem.evaluate(x, return_as_dictionary=True)
97 |
98 | outputs = {}
99 | for i in range(self._problem.n_obj):
100 | outputs[f'objFuncs/f{i}/F'] = out['F'][:, i]
101 | for i in range(self._problem.n_ieq_constr):
102 | outputs[f'constrFuncs/g{i}/F'] = out['G'][:, i]
103 | return outputs
104 |
105 | @property
106 | def pop(self) -> Population:
107 | f, g, idx = self.opt_prob.CreateObjAndConstrMatrices()
108 | x = self.opt_prob.inputMatrix[idx]
109 |
110 | kwargs = {'X': x, 'F': f}
111 | if self._problem.n_ieq_constr > 0:
112 | kwargs['G'] = g
113 | return Population.new(**kwargs)
114 |
115 | def _get_infill(self):
116 | if self._problem.n_obj == 1:
117 | return 'EI'
118 |
119 | elif self._problem.n_obj == 2:
120 | return 'EHVI2D'
121 | return 'WFGEHVI'
122 |
123 | def _get_convergence(self):
124 | if self._problem.n_obj == 1:
125 | return [
126 | CC.AbsOptXChange(1e-8, 5),
127 | CC.MinInfillValue(1e-6, 4),
128 | ]
129 |
130 | return [
131 | CC.StallIterations(5),
132 | ]
133 |
134 | def optimize(self):
135 | """Run the optimization loop for n_infill infill points (on top on the initialization points)"""
136 | optimizer = self.optimizer
137 | optimizer.Optimize(
138 | nMaxIters=self._n_infill,
139 | listConvCrit=self._get_convergence(),
140 | infillMethod=self._get_infill(),
141 | )
142 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/pymoo_interface/md_mating.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | import math
26 | import numpy as np
27 |
28 | from pymoo.core.individual import Individual
29 | from pymoo.core.infill import InfillCriterion
30 | from pymoo.core.population import Population
31 | from pymoo.core.problem import Problem
32 | from pymoo.core.variable import Choice, Real, Integer, Binary
33 | from pymoo.operators.crossover.sbx import SBX
34 | from pymoo.operators.crossover.ux import UX
35 | from pymoo.operators.mutation.bitflip import BFM
36 | from pymoo.operators.mutation.pm import PM
37 | from pymoo.operators.mutation.rm import ChoiceRandomMutation
38 | from pymoo.operators.repair.rounding import RoundingRepair
39 | from pymoo.operators.selection.rnd import RandomSelection
40 |
41 | __all__ = ['MixedDiscreteMating']
42 |
43 |
44 | class MixedDiscreteMating(InfillCriterion):
45 | """SBArchOpt implementation of mixed-discrete mating (crossover and mutation) operations. Similar functionality as
46 | `pymoo.core.mixed.MixedVariableMating`, however keeps x as a matrix."""
47 |
48 | def __init__(self,
49 | selection=RandomSelection(),
50 | crossover=None,
51 | mutation=None,
52 | repair=None,
53 | eliminate_duplicates=True,
54 | n_max_iterations=100,
55 | **kwargs):
56 |
57 | super().__init__(repair, eliminate_duplicates, n_max_iterations, **kwargs)
58 |
59 | if crossover is None:
60 | crossover = {
61 | Binary: UX(),
62 | Real: SBX(),
63 | Integer: SBX(vtype=float, repair=RoundingRepair()),
64 | Choice: UX(),
65 | }
66 |
67 | if mutation is None:
68 | mutation = {
69 | Binary: BFM(),
70 | Real: PM(),
71 | Integer: PM(vtype=float, repair=RoundingRepair()),
72 | Choice: ChoiceRandomMutation(),
73 | }
74 |
75 | self.selection = selection
76 | self.crossover = crossover
77 | self.mutation = mutation
78 |
79 | def _do(self, problem, pop, n_offsprings, parents=False, **kwargs):
80 |
81 | # So far we assume all crossover need the same amount of parents and create the same number of offsprings
82 | n_parents_crossover = 2
83 | n_offspring_crossover = 2
84 |
85 | # the variables with the concrete information
86 | var_defs = problem.vars
87 |
88 | # group all the variables by their types
89 | vars_by_type = {}
90 | for ik, (k, v) in enumerate(var_defs.items()):
91 | clazz = type(v)
92 |
93 | if clazz not in vars_by_type:
94 | vars_by_type[clazz] = []
95 | vars_by_type[clazz].append((ik, k))
96 |
97 | # # all different recombinations (the choices need to be split because of data types)
98 | recomb = []
99 | for clazz, list_of_vars in vars_by_type.items():
100 | if clazz == Choice:
101 | for idx, var_name in list_of_vars:
102 | recomb.append((clazz, [var_name], np.array([idx])))
103 | else:
104 | idx, var_names = zip(*list_of_vars)
105 | recomb.append((clazz, var_names, np.array(idx)))
106 |
107 | # create an empty population that will be set in each iteration
108 | x_out = np.empty((n_offsprings, len(var_defs)))
109 |
110 | if not parents:
111 | n_select = math.ceil(n_offsprings / n_offspring_crossover)
112 | pop = self.selection(problem, pop, n_select, n_parents_crossover, **kwargs)
113 |
114 | for clazz, list_of_vars, x_idx in recomb:
115 |
116 | crossover = self.crossover[clazz]
117 | assert crossover.n_parents == n_parents_crossover and crossover.n_offsprings == n_offspring_crossover
118 |
119 | _parents = [Population.new(X=[parent.X[x_idx].astype(float) for parent in parents]) for parents in pop]
120 | # _parents = [[Individual(X=parent.X[x_idx]) for parent in parents] for parents in pop]
121 |
122 | _vars = {i: var_defs[e] for i, e in enumerate(list_of_vars)}
123 | _xl, _xu = None, None
124 |
125 | if clazz in [Real, Integer]:
126 | _xl, _xu = np.array([v.bounds for v in _vars.values()]).T
127 |
128 | _problem = Problem(vars=_vars, xl=_xl, xu=_xu)
129 |
130 | while True:
131 | _off = crossover(_problem, _parents, **kwargs)
132 |
133 | mutation = self.mutation[clazz]
134 | _off = mutation(_problem, _off, **kwargs)
135 |
136 | # Sometimes NaN's might sneak into the outputs, try again if this is the case
137 | x_off = _off.get('X')[:n_offsprings, :].astype(float)
138 | if np.any(np.isnan(x_off)):
139 | continue
140 | break
141 |
142 | x_out[:, x_idx] = x_off
143 |
144 | return Population.new(X=x_out)
145 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/hebo_interface/algo.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | import logging
26 | import numpy as np
27 | import pandas as pd
28 | from typing import *
29 | import pymoo.core.variable as var
30 | from pymoo.core.population import Population
31 | from sb_arch_opt.problem import ArchOptProblemBase
32 |
33 | try:
34 | from hebo.design_space.param import Parameter
35 | from hebo.design_space.design_space import DesignSpace
36 | from hebo.optimizers.hebo import HEBO
37 | from hebo.optimizers.general import GeneralBO
38 | from hebo.optimizers.abstract_optimizer import AbstractOptimizer
39 |
40 | HAS_HEBO = True
41 | except ImportError:
42 | HAS_HEBO = False
43 |
44 | __all__ = ['HAS_HEBO', 'check_dependencies', 'HEBOArchOptInterface']
45 |
46 | log = logging.getLogger('sb_arch_opt.hebo')
47 |
48 |
49 | def check_dependencies():
50 | if not HAS_HEBO:
51 | raise ImportError('HEBO dependencies not installed: python setup.py install[hebo]')
52 |
53 |
54 | class HEBOArchOptInterface:
55 | """
56 | Interface class to HEBO algorithm.
57 | """
58 |
59 | def __init__(self, problem: ArchOptProblemBase, n_init: int, seed: int = None):
60 | check_dependencies()
61 | self._problem = problem
62 | self._n_init = n_init
63 | self._optimizer = None
64 | self._design_space = None
65 |
66 | self._seed = seed
67 | if seed is not None:
68 | np.random.seed(seed)
69 |
70 | @property
71 | def problem(self):
72 | return self._problem
73 |
74 | @property
75 | def n_batch(self):
76 | n_batch = self._problem.get_n_batch_evaluate()
77 | return n_batch if n_batch is not None else 1
78 |
79 | @property
80 | def design_space(self) -> 'DesignSpace':
81 | if self._design_space is None:
82 | hebo_var_defs = []
83 | for i, var_def in enumerate(self._problem.des_vars):
84 | name = f'x{i}'
85 |
86 | if isinstance(var_def, var.Real):
87 | hebo_var_defs.append({'name': name, 'type': 'num', 'lb': var_def.bounds[0], 'ub': var_def.bounds[1]})
88 |
89 | elif isinstance(var_def, var.Integer):
90 | hebo_var_defs.append({'name': name, 'type': 'int', 'lb': var_def.bounds[0], 'ub': var_def.bounds[1]})
91 |
92 | elif isinstance(var_def, var.Binary):
93 | hebo_var_defs.append({'name': name, 'type': 'bool'})
94 |
95 | elif isinstance(var_def, var.Choice):
96 | hebo_var_defs.append({'name': name, 'type': 'cat', 'categories': var_def.options})
97 |
98 | else:
99 | raise RuntimeError(f'Unsupported design variable type: {var_def!r}')
100 |
101 | self._design_space = DesignSpace().parse(hebo_var_defs)
102 | return self._design_space
103 |
104 | @property
105 | def optimizer(self) -> 'AbstractOptimizer':
106 | if self._optimizer is None:
107 | if self._problem.n_obj == 1 and self._problem.n_ieq_constr == 0:
108 | self._optimizer = HEBO(self.design_space, model_name='gpy', rand_sample=self._n_init,
109 | scramble_seed=self._seed)
110 | else:
111 | self._optimizer = GeneralBO(self.design_space, num_obj=self._problem.n_obj,
112 | num_constr=self._problem.n_ieq_constr, rand_sample=self._n_init,
113 | model_config={'num_epochs': 100})
114 | return self._optimizer
115 |
116 | @property
117 | def pop(self) -> Population:
118 | x = self._to_x(self.optimizer.X)
119 |
120 | y: np.ndarray = self.optimizer.y
121 | f = y[:, :self._problem.n_obj]
122 | kwargs = {'X': x, 'F': f}
123 | if self._problem.n_ieq_constr > 0:
124 | kwargs['G'] = y[:, self._problem.n_obj:]
125 |
126 | return Population.new(**kwargs)
127 |
128 | def optimize(self, n_infill: int):
129 | """Run the optimization loop for n_infill infill points (on top on the initialization points)"""
130 | n_total = self._n_init+n_infill
131 | evaluated = 0
132 | while evaluated < n_total:
133 | x = self.ask()
134 |
135 | out = self._problem.evaluate(x, return_as_dictionary=True)
136 | x_eval = out['X']
137 | f = out['F']
138 | g = out['G'] if self._problem.n_ieq_constr > 0 else None
139 |
140 | self.tell(x_eval, f, g)
141 | evaluated += x_eval.shape[0]
142 |
143 | def ask(self) -> np.ndarray:
144 | """Returns n_batch infill points"""
145 | x_df = self.optimizer.suggest(n_suggestions=self.n_batch)
146 | return self._to_x(x_df)
147 |
148 | def tell(self, x: np.ndarray, f: np.ndarray, g: np.ndarray = None):
149 | """Updates optimizer with evaluated design points"""
150 | y = f
151 | if g is not None:
152 | y = np.column_stack([f, g])
153 |
154 | params: List['Parameter'] = self.design_space.paras.values()
155 | x_df = pd.DataFrame({f'x{i}': param.inverse_transform(x[:, i]) for i, param in enumerate(params)})
156 |
157 | self.optimizer.observe(x_df, y)
158 |
159 | def _to_x(self, x_df: pd.DataFrame) -> np.ndarray:
160 | params: List['Parameter'] = self.design_space.paras.values()
161 | return np.column_stack([param.transform(x_df[f'x{i}']) for i, param in enumerate(params)])
162 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/test_design_space.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | import numpy as np
3 | from sb_arch_opt.problem import *
4 | from sb_arch_opt.sampling import *
5 | from sb_arch_opt.design_space import *
6 | from pymoo.core.variable import Real, Integer, Binary, Choice
7 |
8 |
9 | class DesignSpaceTest(ArchDesignSpace):
10 |
11 | def __init__(self, des_vars):
12 | self._des_vars = des_vars
13 | super().__init__()
14 |
15 | def is_explicit(self) -> bool:
16 | return False
17 |
18 | def _get_variables(self):
19 | return self._des_vars
20 |
21 | def _quick_sample_discrete_x(self, n: int):
22 | """Sample n discrete design vectors (also return is_active) without generating all design vectors first"""
23 | raise RuntimeError
24 |
25 | def _is_conditionally_active(self):
26 | return [False]*self.n_var
27 |
28 | def _correct_x(self, x: np.ndarray, is_active: np.ndarray):
29 | pass
30 |
31 | def _get_n_valid_discrete(self):
32 | pass
33 |
34 | def _get_n_active_cont_mean(self):
35 | pass
36 |
37 | def _get_n_correct_discrete(self):
38 | pass
39 |
40 | def _get_n_active_cont_mean_correct(self):
41 | pass
42 |
43 | def _gen_all_discrete_x(self):
44 | pass
45 |
46 |
47 | def test_rounding():
48 | ds = DesignSpaceTest([Integer(bounds=(0, 5)), Integer(bounds=(-1, 1)), Integer(bounds=(2, 4))])
49 |
50 | assert np.all(ds.is_discrete_mask)
51 | x = np.array(list(itertools.product(np.linspace(0, 5, 20), np.linspace(-1, 1, 20), np.linspace(2, 4, 20))))
52 | ds.round_x_discrete(x)
53 |
54 | x1, x1_counts = np.unique(x[:, 0], return_counts=True)
55 | assert np.all(x1 == [0, 1, 2, 3, 4, 5])
56 | x1_counts = x1_counts / np.sum(x1_counts)
57 | assert np.all(np.abs(x1_counts - np.mean(x1_counts)) <= .05)
58 |
59 | x2, x2_counts = np.unique(x[:, 1], return_counts=True)
60 | assert np.all(x2 == [-1, 0, 1])
61 | x2_counts = x2_counts / np.sum(x2_counts)
62 | assert np.all(np.abs(x2_counts - np.mean(x2_counts)) <= .05)
63 |
64 | x3, x3_counts = np.unique(x[:, 2], return_counts=True)
65 | assert np.all(x3 == [2, 3, 4])
66 | x3_counts = x3_counts / np.sum(x3_counts)
67 | assert np.all(np.abs(x3_counts - np.mean(x3_counts)) <= .05)
68 |
69 | x_out_of_bounds = np.zeros((20, 3), dtype=int)
70 | x_out_of_bounds[:, 0] = np.linspace(-1, 6, 20)
71 | ds.round_x_discrete(x_out_of_bounds)
72 | assert np.all(np.min(x_out_of_bounds, axis=0) == [0, 0, 2])
73 | assert np.all(np.max(x_out_of_bounds, axis=0) == [5, 0, 2])
74 |
75 |
76 | def test_init_no_vars():
77 | ds = DesignSpaceTest([])
78 | assert ds.n_var == 0
79 | assert ds.des_vars == []
80 | assert ds.get_n_declared_discrete() == 1
81 | assert np.isnan(ds.discrete_imputation_ratio)
82 | assert ds.continuous_imputation_ratio == 1.
83 | assert np.isnan(ds.imputation_ratio)
84 | assert np.isnan(ds.discrete_correction_ratio)
85 | assert ds.continuous_correction_ratio == 1.
86 | assert np.isnan(ds.correction_ratio)
87 |
88 | assert not ds.is_explicit()
89 |
90 |
91 | def test_init_vars():
92 | ds = DesignSpaceTest([
93 | Real(bounds=(1, 5)),
94 | Integer(bounds=(1, 4)),
95 | Binary(),
96 | Choice(options=['A', 'B', 'C']),
97 | ])
98 | assert ds.n_var == 4
99 |
100 | assert np.all(ds.xl == [1, 1, 0, 0])
101 | assert np.all(ds.xu == [5, 4, 1, 2])
102 | assert np.all(ds.is_cat_mask == [False, False, False, True])
103 | assert np.all(ds.is_int_mask == [False, True, True, False])
104 | assert np.all(ds.is_discrete_mask == [False, True, True, True])
105 | assert np.all(ds.is_cont_mask == [True, False, False, False])
106 | assert np.all(~ds.is_conditionally_active)
107 |
108 | assert ds.get_n_declared_discrete() == 4*2*3
109 |
110 |
111 | def test_get_categorical_values():
112 | ds = DesignSpaceTest([Choice(options=['A', 'B', 'C'])])
113 | assert ds.all_discrete_x == (None, None)
114 | x_all, _ = ds.all_discrete_x_by_trial_and_imputation
115 | assert x_all.shape == (3, 1)
116 |
117 | x_all_, _ = ds.all_discrete_x
118 | assert np.all(x_all_ == x_all)
119 |
120 | cat_values = ds.get_categorical_values(x_all, 0)
121 | assert len(cat_values) == 3
122 | assert np.all(cat_values == ['A', 'B', 'C'])
123 |
124 |
125 | def test_x_generation(problem: ArchOptProblemBase, discrete_problem: ArchOptProblemBase):
126 | for prob, n_valid, n_correct, cont_imp_ratio in [
127 | (problem, 10*10, 10*10, 1.2),
128 | (discrete_problem, 10*5+5, 10*10, 1.),
129 | ]:
130 | ds = prob.design_space
131 | assert ds.get_n_declared_discrete() == 10*10
132 |
133 | assert ds.get_n_valid_discrete() == n_valid
134 | assert ds.discrete_imputation_ratio == (10 * 10) / n_valid
135 | assert ds.continuous_imputation_ratio == cont_imp_ratio
136 | assert ds.imputation_ratio == cont_imp_ratio * (10*10)/n_valid
137 |
138 | assert ds.get_n_correct_discrete() == n_correct
139 | assert ds.discrete_correction_ratio == (10 * 10) / n_correct
140 | assert ds.continuous_correction_ratio == cont_imp_ratio
141 | assert ds.correction_ratio == cont_imp_ratio * (10*10)/n_correct
142 |
143 | assert ds.corrector is not None
144 | assert not ds.use_auto_corrector
145 |
146 | assert not ds.is_explicit()
147 |
148 | x_discrete, is_active_discrete = ds.all_discrete_x
149 | assert x_discrete.shape[0] == ds.get_n_valid_discrete()
150 | assert is_active_discrete.shape[0] == ds.get_n_valid_discrete()
151 | assert np.all(~LargeDuplicateElimination.eliminate(x_discrete))
152 | ds.get_discrete_rates(show=True)
153 |
154 | x_discrete_trial, is_act_trail = ds.all_discrete_x_by_trial_and_imputation
155 | assert np.all(x_discrete_trial == x_discrete)
156 | assert np.all(is_active_discrete == is_act_trail)
157 |
158 | np.random.seed(None)
159 | for _ in range(10):
160 | x_sampled, is_act_sampled = ds.quick_sample_discrete_x(20)
161 | assert x_sampled.shape == (20, ds.n_var)
162 | assert is_act_sampled.shape == (20, ds.n_var)
163 |
164 | x_sampled_, is_act_sampled_ = ds.correct_x(x_sampled)
165 | assert np.all(x_sampled_ == x_sampled)
166 | assert np.all(is_act_sampled_ == is_act_sampled)
167 |
168 | np.random.seed(42)
169 | x1, _ = ds.quick_sample_discrete_x(20)
170 | x2, _ = ds.quick_sample_discrete_x(20)
171 | assert np.any(x1 != x2)
172 |
173 | np.random.seed(42)
174 | x3, _ = ds.quick_sample_discrete_x(20)
175 | assert np.all(x1 == x3)
176 |
--------------------------------------------------------------------------------
/sb_arch_opt/problems/md_mo.py:
--------------------------------------------------------------------------------
1 | """
2 | Licensed under the GNU General Public License, Version 3.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | https://www.gnu.org/licenses/gpl-3.0.html.en
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 |
14 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
15 | Contact: jasper.bussemaker@dlr.de
16 |
17 | This test suite contains a set of mixed-discrete multi-objective problems.
18 | """
19 | import numpy as np
20 | from pymoo.core.variable import Real
21 | from pymoo.problems.multi.zdt import ZDT1
22 | from pymoo.problems.single.himmelblau import Himmelblau as HB
23 | from sb_arch_opt.problems.continuous import *
24 | from sb_arch_opt.problems.problems_base import *
25 |
26 | __all__ = ['MOHimmelblau', 'MDMOHimmelblau', 'DMOHimmelblau', 'MOGoldstein', 'MDMOGoldstein',
27 | 'DMOGoldstein', 'MOZDT1', 'MDZDT1', 'DZDT1', 'MDZDT1Small', 'MDZDT1Mid', 'MORosenbrock', 'MDMORosenbrock']
28 |
29 |
30 | class MOHimmelblau(NoHierarchyProblemBase):
31 | """Multi-objective version of the Himmelblau test problem"""
32 |
33 | def __init__(self):
34 | self._problem = problem = HB()
35 | des_vars = [Real(bounds=(problem.xl[i], problem.xu[i])) for i in range(problem.n_var)]
36 | super().__init__(des_vars, n_obj=2)
37 |
38 | def _arch_evaluate(self, x: np.ndarray, is_active_out: np.ndarray, f_out: np.ndarray, g_out: np.ndarray,
39 | h_out: np.ndarray, *args, **kwargs):
40 |
41 | f_out[:, 0] = self._problem.evaluate(x, return_as_dictionary=True)['F'][:, 0]
42 | f_out[:, 1] = self._problem.evaluate(x[:, ::-1], return_as_dictionary=True)['F'][:, 0]
43 |
44 |
45 | class MDMOHimmelblau(MixedDiscretizerProblemBase):
46 | """Mixed-discrete version of the multi-objective Himmelblau test problem"""
47 |
48 | def __init__(self):
49 | super().__init__(MOHimmelblau(), n_vars_int=1)
50 |
51 |
52 | class DMOHimmelblau(MixedDiscretizerProblemBase):
53 | """Discrete version of the multi-objective Himmelblau test problem"""
54 |
55 | def __init__(self):
56 | super().__init__(MOHimmelblau())
57 |
58 |
59 | class MOGoldstein(NoHierarchyProblemBase):
60 | """Multi-objective version of the Goldstein test problem"""
61 |
62 | def __init__(self):
63 | self._problem = problem = Goldstein()
64 | super().__init__(problem.des_vars, n_obj=2)
65 |
66 | def _arch_evaluate(self, x: np.ndarray, is_active_out: np.ndarray, f_out: np.ndarray, g_out: np.ndarray,
67 | h_out: np.ndarray, *args, **kwargs):
68 | f_out[:, 0] = self._problem.evaluate(x, return_as_dictionary=True)['F'][:, 0]
69 | f_out[:, 1] = -self._problem.evaluate(x+.25, return_as_dictionary=True)['F'][:, 0]
70 |
71 |
72 | class MDMOGoldstein(MixedDiscretizerProblemBase):
73 | """Mixed-discrete version of the multi-objective Goldstein test problem"""
74 |
75 | def __init__(self):
76 | super().__init__(MOGoldstein(), n_vars_int=1)
77 |
78 |
79 | class DMOGoldstein(MixedDiscretizerProblemBase):
80 | """Discrete version of the multi-objective Goldstein test problem"""
81 |
82 | def __init__(self):
83 | super().__init__(MOGoldstein())
84 |
85 |
86 | class MORosenbrock(NoHierarchyProblemBase):
87 | """Multi-objective version of the Rosenbrock problem"""
88 |
89 | def __init__(self, n_var=10):
90 | self._rosenbrock = problem = Rosenbrock(n_var=n_var)
91 | des_vars = problem.des_vars
92 | super().__init__(des_vars, n_obj=2)
93 |
94 | def _arch_evaluate(self, x: np.ndarray, is_active_out: np.ndarray, f_out: np.ndarray, g_out: np.ndarray,
95 | h_out: np.ndarray, *args, **kwargs):
96 | out = self._rosenbrock.evaluate(x, return_as_dictionary=True)
97 | f_out[:, 0] = f1 = out['F'][:, 0]
98 | f_out[:, 1] = .1*(np.abs((6000-f1)/40)**2 + np.sum((x[:, :4]+1)**2*2000, axis=1))
99 |
100 |
101 | class MDMORosenbrock(MixedDiscretizerProblemBase):
102 | """Mixed-discrete multi-objective Rosenbrock problem"""
103 |
104 | def __init__(self):
105 | super().__init__(MORosenbrock(), n_opts=4, n_vars_int=5)
106 |
107 |
108 | class MDZDT1Small(MixedDiscretizerProblemBase):
109 | """Mixed-discrete version of the multi-objective ZDT1 test problem"""
110 |
111 | def __init__(self):
112 | super().__init__(ZDT1(n_var=12), n_opts=3, n_vars_int=6)
113 |
114 |
115 | class MDZDT1Mid(MixedDiscretizerProblemBase):
116 | """Mixed-discrete version of the multi-objective ZDT1 test problem"""
117 |
118 | def __init__(self):
119 | super().__init__(ZDT1(n_var=20), n_opts=3, n_vars_int=10)
120 |
121 |
122 | class MOZDT1(NoHierarchyWrappedProblem):
123 | """Wrapper for ZDT1 test problem"""
124 |
125 | def __init__(self):
126 | super().__init__(ZDT1())
127 |
128 |
129 | class MDZDT1(MixedDiscretizerProblemBase):
130 | """Mixed-discrete version of the multi-objective ZDT1 test problem"""
131 |
132 | def __init__(self):
133 | super().__init__(ZDT1(), n_opts=5, n_vars_int=15)
134 |
135 |
136 | class DZDT1(MixedDiscretizerProblemBase):
137 | """Discrete version of the multi-objective ZDT1 test problem"""
138 |
139 | def __init__(self):
140 | super().__init__(ZDT1(), n_opts=5)
141 |
142 |
143 | if __name__ == '__main__':
144 | # MOHimmelblau().print_stats()
145 | # MDMOHimmelblau().print_stats()
146 | # MDMOHimmelblau().plot_design_space()
147 | # DMOHimmelblau().print_stats()
148 | # # MOHimmelblau().plot_pf()
149 | # # MDMOHimmelblau().plot_pf()
150 | # DMOHimmelblau().plot_pf()
151 |
152 | # MOGoldstein().print_stats()
153 | # MOGoldstein().plot_design_space()
154 | # MDMOGoldstein().print_stats()
155 | # MDMOGoldstein().plot_design_space()
156 | # DMOGoldstein().print_stats()
157 | # DMOGoldstein().plot_design_space()
158 | # # MOGoldstein().plot_pf()
159 | # # MDMOGoldstein().plot_pf()
160 | # DMOGoldstein().plot_pf()
161 |
162 | MORosenbrock().print_stats()
163 | # MORosenbrock().plot_pf()
164 | # MDMORosenbrock().print_stats()
165 | # MDMORosenbrock().plot_pf()
166 |
167 | # MOZDT1().print_stats()
168 | # MDZDT1().print_stats()
169 | # MDZDT1Small().print_stats()
170 | # MDZDT1Mid().print_stats()
171 | # DZDT1().print_stats()
172 | # # MOZDT1().plot_pf()
173 | # # MDZDT1().plot_pf()
174 | # DZDT1().plot_pf()
175 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/test_correction.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from typing import *
3 | from pymoo.core.variable import Real, Choice
4 | from sb_arch_opt.design_space import ArchDesignSpace
5 | from sb_arch_opt.correction import *
6 |
7 |
8 | class DummyArchDesignSpace(ArchDesignSpace):
9 |
10 | def __init__(self, x_all: np.ndarray, is_act_all: np.ndarray, is_discrete_mask: np.ndarray = None):
11 | self._corrector = None
12 | self._x_all = x_all
13 | self._is_act_all = is_act_all
14 |
15 | if is_discrete_mask is None:
16 | is_discrete_mask = np.ones((x_all.shape[1],), dtype=bool)
17 | self._is_discrete_mask = is_discrete_mask
18 | super().__init__()
19 |
20 | self.use_auto_corrector = True
21 |
22 | @property
23 | def corrector(self):
24 | if self._corrector is None:
25 | self._corrector = self._get_corrector()
26 | return self._corrector
27 |
28 | @corrector.setter
29 | def corrector(self, corrector):
30 | self._corrector = corrector
31 |
32 | def is_explicit(self) -> bool:
33 | return False
34 |
35 | def _get_variables(self):
36 | des_vars = []
37 | for i, is_discrete in enumerate(self._is_discrete_mask):
38 | if is_discrete:
39 | des_vars.append(Choice(options=list(sorted(np.unique(self._x_all[:, i])))))
40 | else:
41 | des_vars.append(Real(bounds=(0., 1.)))
42 | return des_vars
43 |
44 | def _is_conditionally_active(self) -> Optional[List[bool]]:
45 | pass # Derived from is_active_all
46 |
47 | def _correct_x(self, x: np.ndarray, is_active: np.ndarray):
48 | raise RuntimeError
49 |
50 | def _quick_sample_discrete_x(self, n: int) -> Tuple[np.ndarray, np.ndarray]:
51 | raise RuntimeError
52 |
53 | def _get_n_valid_discrete(self) -> Optional[int]:
54 | return self._x_all.shape[0]
55 |
56 | def _get_n_active_cont_mean(self) -> Optional[float]:
57 | pass
58 |
59 | def _gen_all_discrete_x(self) -> Optional[Tuple[np.ndarray, np.ndarray]]:
60 | return self._x_all, self._is_act_all
61 |
62 | def is_valid(self, xi: np.ndarray) -> Optional[np.ndarray]:
63 | eager_corr = EagerCorrectorBase(self)
64 | i_valid = eager_corr.get_correct_idx(np.array([xi]))[0]
65 | if i_valid == -1:
66 | return
67 | _, is_active_valid = eager_corr.x_valid_active
68 | return is_active_valid[i_valid, :]
69 |
70 |
71 | def test_corrector():
72 | x_all = np.array([[0, 0],
73 | [1, 0],
74 | [0, 1],
75 | [1, 1]])
76 | is_act_all = np.ones(x_all.shape, dtype=bool)
77 | ds = DummyArchDesignSpace(x_all=x_all, is_act_all=is_act_all)
78 | assert np.all(ds.is_discrete_mask)
79 |
80 | corr = CorrectorBase(ds)
81 | assert np.all(corr.is_discrete_mask)
82 | assert np.all(corr.x_imp_discrete == np.array([0, 0]))
83 |
84 | assert corr._is_canonical_inactive(np.array([1, 1]), np.array([True, True]))
85 | assert corr._is_canonical_inactive(np.array([1, 0]), np.array([True, False]))
86 | assert not corr._is_canonical_inactive(np.array([1, 1]), np.array([True, False]))
87 |
88 |
89 | def test_eager_corrector():
90 | x_all = np.array([[0, 0],
91 | [0, 1],
92 | [0, 2],
93 | [1, 0],
94 | [1, 1],
95 | [2, 0]])
96 | is_act_all = np.ones(x_all.shape, dtype=bool)
97 | is_act_all[-1, 1] = False
98 | ds = DummyArchDesignSpace(x_all=x_all, is_act_all=is_act_all)
99 |
100 | corr = EagerCorrectorBase(ds)
101 | x_try = np.array([[1, 0], # Canonical, 3
102 | [1, 2], # Invalid
103 | [2, 0], # Canonical, 5
104 | [2, 1]]) # Valid, non-canonical, 5
105 |
106 | assert np.all(corr.get_correct_idx(x_try) == np.array([3, -1, 5, 5]))
107 | assert np.all(corr.get_canonical_idx(x_try) == np.array([3, -1, 5, -1]))
108 |
109 |
110 | def test_closest_eager_corrector():
111 | x_all = np.array([[0, 0, 0],
112 | [0, 0, 1],
113 | [0, 1, 0],
114 | [0, 1, 1],
115 | [0, 1, 2],
116 | [1, 0, 0],
117 | [1, 0, 2],
118 | [1, 1, 0],
119 | [2, 0, 0],
120 | [2, 1, 3]])
121 | is_act_all = np.ones(x_all.shape, dtype=bool)
122 | is_act_all[-3, 2] = False
123 | is_act_all[-2, 1:] = False
124 | ds = DummyArchDesignSpace(x_all=x_all, is_act_all=is_act_all)
125 |
126 | for correct_correct_x in [False, True]:
127 | for random_if_multiple in [False, True]:
128 | for _ in range(10 if random_if_multiple else 1):
129 | for euclidean in [False, True]:
130 | ds.corrector = corr = ClosestEagerCorrector(
131 | ds, euclidean=euclidean, correct_correct_x=correct_correct_x,
132 | random_if_multiple=random_if_multiple)
133 | assert repr(corr)
134 |
135 | x_corr, is_act_corr = ds.correct_x(np.array([[0, 0, 0],
136 | [0, 0, 3],
137 | [1, 0, 1],
138 | [1, 1, 1],
139 | [1, 1, 2],
140 | [2, 0, 2]]))
141 |
142 | x_corr_, is_act_corr_ = ds.correct_x(x_corr)
143 | assert np.all(x_corr == x_corr_)
144 | assert np.all(is_act_corr == is_act_corr_)
145 |
146 | corr_first = np.array([[0, 0, 0],
147 | [0, 0, 1],
148 | [1, 0, 0],
149 | [1, 1, 0],
150 | [1, 1, 0],
151 | [2, 0, 0]])
152 | if euclidean:
153 | corr_first[1, :] = [0, 1, 2]
154 | if correct_correct_x:
155 | corr_first[-2, :] = [1, 0, 2]
156 | corr_first[-1, :] = [1, 0, 2]
157 | corr_second = corr_first.copy()
158 | corr_second[2, :] = [1, 0, 2]
159 |
160 | if random_if_multiple:
161 | assert np.all(x_corr == corr_first) or np.all(x_corr == corr_second)
162 | else:
163 | assert np.all(x_corr == corr_first)
164 |
--------------------------------------------------------------------------------
/sb_arch_opt/problems/constrained.py:
--------------------------------------------------------------------------------
1 | """
2 | Licensed under the GNU General Public License, Version 3.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | https://www.gnu.org/licenses/gpl-3.0.html.en
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 |
14 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
15 | Contact: jasper.bussemaker@dlr.de
16 |
17 | This test suite contains a set of continuous and mixed-discrete, multi-objective, constrained problems.
18 | """
19 | import numpy as np
20 | from pymoo.core.variable import Real
21 | from pymoo.problems.multi.osy import OSY
22 | from pymoo.problems.multi.carside import Carside
23 | from pymoo.problems.multi.welded_beam import WeldedBeam
24 | from pymoo.problems.multi.dascmop import DASCMOP7, DIFFICULTIES
25 | from pymoo.problems.single.cantilevered_beam import CantileveredBeam
26 | from sb_arch_opt.problems.problems_base import *
27 |
28 | __all__ = ['ArchCantileveredBeam', 'MDCantileveredBeam', 'ArchWeldedBeam', 'MDWeldedBeam', 'ArchCarside', 'MDCarside',
29 | 'ArchOSY', 'MDOSY', 'MODASCMOP', 'MDDASCMOP', 'ConBraninProd', 'ConBraninGomez']
30 |
31 |
32 | class ArchCantileveredBeam(NoHierarchyWrappedProblem):
33 |
34 | def __init__(self):
35 | super().__init__(CantileveredBeam())
36 |
37 |
38 | class MDCantileveredBeam(MixedDiscretizerProblemBase):
39 |
40 | def __init__(self):
41 | super().__init__(ArchCantileveredBeam(), n_vars_int=2)
42 |
43 |
44 | class ArchWeldedBeam(NoHierarchyWrappedProblem):
45 | """Welded beam test problem: https://pymoo.org/problems/multi/welded_beam.html"""
46 |
47 | def __init__(self):
48 | super().__init__(WeldedBeam())
49 |
50 |
51 | class MDWeldedBeam(MixedDiscretizerProblemBase):
52 | """Mixed-discrete version of the welded beam test problem"""
53 |
54 | def __init__(self):
55 | super().__init__(ArchWeldedBeam(), n_vars_int=2)
56 |
57 |
58 | class ArchCarside(NoHierarchyWrappedProblem):
59 | """Carside test problem"""
60 |
61 | def __init__(self):
62 | super().__init__(Carside())
63 |
64 |
65 | class MDCarside(MixedDiscretizerProblemBase):
66 | """Mixed-discrete version of the Carside test problem"""
67 |
68 | def __init__(self):
69 | super().__init__(ArchCarside(), n_vars_int=4)
70 |
71 |
72 | class ArchOSY(NoHierarchyWrappedProblem):
73 | """OSY test problem: https://pymoo.org/problems/multi/osy.html"""
74 |
75 | def __init__(self):
76 | super().__init__(OSY())
77 |
78 |
79 | class MDOSY(MixedDiscretizerProblemBase):
80 | """Mixed-discrete version of the OSY test problem"""
81 |
82 | def __init__(self):
83 | super().__init__(ArchOSY(), n_vars_int=3)
84 |
85 |
86 | class MODASCMOP(NoHierarchyWrappedProblem):
87 | """A particular instance of the DAS-CMOP 3-objective test problem:
88 | https://pymoo.org/problems/constrained/dascmop.html"""
89 |
90 | def __init__(self):
91 | super().__init__(DASCMOP7(DIFFICULTIES[0]))
92 |
93 |
94 | class MDDASCMOP(MixedDiscretizerProblemBase):
95 | """Mixed-discrete version of the DAS-CMOP test problem"""
96 |
97 | def __init__(self):
98 | super().__init__(MODASCMOP(), n_opts=3, n_vars_int=15)
99 |
100 |
101 | class ConBraninBase(NoHierarchyProblemBase):
102 | """
103 | Constrained Branin function from:
104 | Parr, J., Holden, C.M., Forrester, A.I. and Keane, A.J., 2010. Review of efficient surrogate infill sampling
105 | criteria with constraint handling.
106 | """
107 |
108 | def __init__(self):
109 | des_vars = [
110 | Real(bounds=(-5, 10)),
111 | Real(bounds=(0, 15)),
112 | ]
113 | super().__init__(des_vars, n_ieq_constr=1)
114 |
115 | def _arch_evaluate(self, x: np.ndarray, is_active_out: np.ndarray, f_out: np.ndarray, g_out: np.ndarray,
116 | h_out: np.ndarray, *args, **kwargs):
117 |
118 | x_norm = (x+[5, 0])/15
119 | for i in range(x.shape[0]):
120 | f_out[i, 0] = self._h(x[i, 0], x[i, 1])
121 | g_out[i, 0] = self._g(x_norm[i, 0], x_norm[i, 1])
122 |
123 | @staticmethod
124 | def _h(x1, x2):
125 | t1 = (x2 - (5.1/(4*np.pi**2))*x1**2 + (5/np.pi)*x1 - 6)**2
126 | t2 = 10*(1-1/(8*np.pi))*np.cos(x1) + 10
127 | return t1 + t2 + 5*x2
128 |
129 | def plot(self, show=True):
130 | import matplotlib.pyplot as plt
131 |
132 | xx1, xx2 = np.meshgrid(np.linspace(-5, 10, 100), np.linspace(0, 15, 100))
133 | out = self.evaluate(np.column_stack([xx1.ravel(), xx2.ravel()]), return_as_dictionary=True)
134 | zz = out['F'][:, 0]
135 | zz[out['G'][:, 0] > 0] = np.nan
136 |
137 | plt.figure(), plt.title(f'{self.__class__.__name__}')
138 | plt.colorbar(plt.contourf(xx1, xx2, zz.reshape(xx1.shape), 50, cmap='inferno'))
139 | plt.xlabel('$x_1$'), plt.ylabel('$x_2$')
140 |
141 | if show:
142 | plt.show()
143 |
144 | def _g(self, x1, x2):
145 | raise NotImplementedError
146 |
147 |
148 | class ConBraninProd(ConBraninBase):
149 | """Constrained Branin problem with the product constraint (Eq. 14)"""
150 |
151 | def _g(self, x1, x2):
152 | return .2 - x1*x2
153 |
154 |
155 | class ConBraninGomez(ConBraninBase):
156 | """Constrained Branin problem with the Gomez#3 constraint (Eq. 15)"""
157 |
158 | def _g(self, x1, x2):
159 | x1 = x1*2-1
160 | x2 = x2*2-1
161 | g = (4 - 2.1*x1**2 + (x1**4)/3)*x1**2 + x1*x2 + (-4 + 4*x2**2)*x2**2 + 3*np.sin(6*(1-x1)) + 3*np.sin(6*(1-x2))
162 | return 6-g
163 |
164 |
165 | if __name__ == '__main__':
166 | # ArchCantileveredBeam().print_stats()
167 | # ArchCantileveredBeam().plot_design_space()
168 | # MDCantileveredBeam().print_stats()
169 | # ArchWeldedBeam().print_stats()
170 | # MDWeldedBeam().print_stats()
171 | # # ArchWeldedBeam().plot_pf()
172 | # # MDWeldedBeam().plot_pf()
173 |
174 | # ArchCarside().print_stats()
175 | # MDCarside().print_stats()
176 | # # ArchCarside().plot_pf()
177 | # MDCarside().plot_pf()
178 |
179 | # ArchOSY().print_stats()
180 | # MDOSY().print_stats()
181 | # # ArchOSY().plot_pf()
182 | # MDOSY().plot_pf()
183 |
184 | # MODASCMOP().print_stats()
185 | # MDDASCMOP().print_stats()
186 | # # MODASCMOP().plot_pf()
187 | # MDDASCMOP().plot_pf()
188 |
189 | # ConBraninProd().plot()
190 | ConBraninProd().print_stats()
191 | # ConBraninGomez().plot()
192 | ConBraninGomez().print_stats()
193 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/problems/test_turbofan_arch.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import tempfile
4 | import numpy as np
5 | from sb_arch_opt.sampling import *
6 | from sb_arch_opt.problems.turbofan_arch import *
7 | from sb_arch_opt.algo.pymoo_interface import get_nsga2
8 | from pymoo.optimize import minimize
9 | from pymoo.core.population import Population
10 | from pymoo.core.initialization import Initialization
11 |
12 | def check_dependency():
13 | return pytest.mark.skipif(not HAS_OPEN_TURB_ARCH, reason='Turbofan arch dependencies not installed')
14 |
15 |
16 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
17 | def test_slow_tests():
18 | assert HAS_OPEN_TURB_ARCH
19 |
20 |
21 | @check_dependency()
22 | def test_simple_problem():
23 | problem = SimpleTurbofanArch()
24 | problem.print_stats()
25 |
26 | assert len(HierarchicalExhaustiveSampling(n_cont=1).do(problem, 0)) == problem._get_n_valid_discrete()
27 |
28 | problem.get_discrete_rates(force=True, show=True)
29 |
30 | x_all, is_act_all = problem.design_space.all_discrete_x_by_trial_and_imputation
31 | assert np.all(problem.is_conditionally_active == np.any(~is_act_all, axis=0))
32 | x_all_corr, is_act_all_corr = problem.correct_x(x_all)
33 | assert np.all(x_all_corr == x_all)
34 | assert np.all(is_act_all_corr == is_act_all)
35 |
36 | x_all, is_act_all = problem.all_discrete_x
37 | assert is_act_all is not None
38 | x_all_corr, is_act_all_corr = problem.correct_x(x_all)
39 | assert np.all(x_all_corr == x_all)
40 | assert np.all(is_act_all_corr == is_act_all)
41 |
42 | x = HierarchicalSampling().do(problem, 1000).get('X')
43 | x_corr, is_act = problem.correct_x(x)
44 | assert np.all(x_corr == x)
45 | x_corr2, is_act2 = problem.correct_x(x_corr)
46 | assert np.all(x_corr2 == x_corr)
47 | assert np.all(is_act2 == is_act)
48 |
49 | x_pf = problem.pareto_set()
50 | assert len(x_pf) == 1
51 | f_pf = problem.pareto_front()
52 | assert len(f_pf) == 1
53 |
54 | assert problem._load_evaluated()
55 |
56 | f_eval = problem.evaluate(x_pf[[0], :], return_as_dictionary=True)['F']
57 | assert np.all(np.isfinite(f_eval))
58 | assert np.all(np.abs(f_eval[0, :] - f_pf[0, :]) < 1e-3)
59 |
60 |
61 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
62 | @check_dependency()
63 | def test_simple_problem_eval():
64 | with tempfile.TemporaryDirectory() as tmp_folder:
65 | problem = SimpleTurbofanArch(n_parallel=2)
66 | algo = get_nsga2(pop_size=2, results_folder=tmp_folder)
67 |
68 | algo.initialization = Initialization(Population.new(X=np.array([
69 | [1, 12.34088028, 1.293692084, 2, 49.95762965, 0.333333333, 0.333333333, 14857.31833, 16919.26153, 8325.806323, 0, 3, 0, 0, 0],
70 | [0, 7.25, 1.45, 1, 23.04752703, 0.176817974, 0, 14412.5685, 14436.53056, 10500, 0, 3, 0, 1, 0],
71 | ])))
72 |
73 | result = minimize(problem, algo, termination=('n_eval', 2))
74 | f, g = result.pop.get('F'), result.pop.get('G')
75 | assert np.all(np.abs(f[0, :]-np.array([7.038242017])) < 1e-2)
76 | assert np.all(np.abs(g[0, :]-np.array([-0.219864891, -0.566666667, -11.61994603, -11.61994603, -11.61994603])) < 1e-2)
77 | assert np.isinf(f[1, 0])
78 | assert np.all(np.isinf(g[1, :]))
79 |
80 |
81 | @check_dependency()
82 | def test_simple_problem_model():
83 | problem = SimpleTurbofanArchModel()
84 | problem.print_stats()
85 |
86 | assert len(HierarchicalExhaustiveSampling(n_cont=1).do(problem, 0)) == problem._get_n_valid_discrete()
87 |
88 | problem.get_discrete_rates(force=True, show=True)
89 |
90 | x_all, is_act_all = problem.all_discrete_x
91 | assert is_act_all is not None
92 | out = problem.evaluate(x_all, return_as_dictionary=True)
93 | is_failed = np.where(problem.get_failed_points(out))[0]
94 | assert len(is_failed) == 67
95 |
96 | x_model_best = np.array([[1.00000000e+00, 1.24341485e+01, 1.31267687e+00, 2.00000000e+00,
97 | 5.71184472e+01, 5.00000000e-01, 5.00000000e-01, 8.39151709e+03,
98 | 1.05000000e+04, 7.85928873e+03, 1.00000000e+00, 3.00000000e+00,
99 | 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]])
100 | out = problem.evaluate(x_model_best, return_as_dictionary=True)
101 | assert out['F'][0, 0] == pytest.approx(6.79, abs=1e-2)
102 |
103 | x = HierarchicalSampling().do(problem, 1000).get('X')
104 | problem.evaluate(x, return_as_dictionary=True)
105 |
106 |
107 | @check_dependency()
108 | def test_realistic_problem():
109 | problem = RealisticTurbofanArch()
110 | problem.print_stats()
111 |
112 | assert problem._get_n_valid_discrete() == 142243
113 | problem.get_discrete_rates(show=True) # Takes several minutes
114 | x_all, is_act_all = problem.all_discrete_x
115 | assert x_all.shape[0] == problem.get_n_valid_discrete()
116 |
117 | i_random = np.random.choice(x_all.shape[0], 1000, replace=False)
118 | x_all_corr, is_act_all_corr = problem.correct_x(x_all[i_random])
119 | assert np.all(x_all_corr == x_all[i_random])
120 | assert np.all(is_act_all_corr == is_act_all[i_random])
121 |
122 | x = HierarchicalSampling().do(problem, 100).get('X')
123 | x_corr, is_act = problem.correct_x(x)
124 | assert np.all(x_corr == x)
125 | x_corr2, is_act2 = problem.correct_x(x_corr)
126 | assert np.all(x_corr2 == x_corr)
127 | assert np.all(is_act2 == is_act)
128 |
129 | f_pf = problem.pareto_front()
130 | x_pf = problem.pareto_set()
131 | x_pf_corr, is_act_pf = problem.correct_x(x_pf)
132 | assert np.all(x_pf_corr == x_pf)
133 | assert not np.all(is_act_pf)
134 | assert problem._load_evaluated()
135 |
136 | f_eval = problem.evaluate(x_pf[[0], :], return_as_dictionary=True)['F']
137 | assert np.all(np.isfinite(f_eval))
138 | assert np.all(np.abs(f_eval[0, :] - f_pf[0, :]) < 1e-3)
139 |
140 |
141 | @pytest.mark.skipif(int(os.getenv('RUN_SLOW_TESTS', 0)) != 1, reason='Set RUN_SLOW_TESTS=1 to run slow tests')
142 | @check_dependency()
143 | def test_realistic_problem_2obj():
144 | problem = RealisticTurbofanArch(noise_obj=False)
145 | assert problem.n_obj == 2
146 | problem.print_stats()
147 |
148 | assert problem._get_n_valid_discrete() == 142243
149 | x_all, is_act_all = problem.all_discrete_x
150 | assert x_all.shape[0] == problem.get_n_valid_discrete()
151 |
152 | f_pf = problem.pareto_front()
153 | assert f_pf.shape[1] == 2
154 | x_pf = problem.pareto_set()
155 | assert f_pf.shape[0] == x_pf.shape[0]
156 | x_pf_corr, is_act_pf = problem.correct_x(x_pf)
157 | assert np.all(x_pf_corr == x_pf)
158 | assert not np.all(is_act_pf)
159 | assert problem._load_evaluated()
160 |
161 | f_eval = problem.evaluate(x_pf[[0], :], return_as_dictionary=True)['F']
162 | assert np.all(np.isfinite(f_eval))
163 | assert np.all(np.abs(f_eval[0, :] - f_pf[0, :]) < 1e-3)
164 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/pymoo_interface/api.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | import logging
26 | from pymoo.core.algorithm import Algorithm
27 | from pymoo.core.population import Population
28 | from pymoo.algorithms.moo.nsga2 import NSGA2, RankAndCrowdingSurvival
29 | from pymoo.termination.max_eval import MaximumFunctionCallTermination
30 |
31 | from sb_arch_opt.sampling import *
32 | from sb_arch_opt.util import capture_log, set_global_random_seed
33 | from sb_arch_opt.problem import ArchOptRepair
34 | from sb_arch_opt.algo.pymoo_interface.metrics import *
35 | from sb_arch_opt.algo.pymoo_interface.md_mating import *
36 | from sb_arch_opt.algo.pymoo_interface.storage_restart import *
37 |
38 | __all__ = ['provision_pymoo', 'ArchOptNSGA2', 'get_nsga2', 'initialize_from_previous_results', 'ResultsStorageCallback',
39 | 'ArchOptEvaluator', 'get_default_termination', 'DeltaHVTermination', 'ArchOptEvaluator',
40 | 'load_from_previous_results', 'get_doe_algo', 'DOEAlgorithm', 'plot']
41 |
42 | log = logging.getLogger('sb_arch_opt.pymoo')
43 |
44 |
45 | def plot(*args, **kwargs):
46 | try:
47 | from pymoo.util.plotting import plot
48 | except ModuleNotFoundError:
49 | from pymoo.visualization.util import plot # pymoo >= 0.6.1.6
50 |
51 | return plot(*args, **kwargs)
52 |
53 |
54 | def provision_pymoo(algorithm: Algorithm, set_init=True, results_folder=None):
55 | """
56 | Provisions a pymoo Algorithm to work correctly for architecture optimization:
57 | - Sets initializer using a repaired sampler (if `set_init = True`)
58 | - Sets a repair operator
59 | - Optionally stores intermediate and final results in some results folder
60 | - Replace NaN outputs with Inf
61 | """
62 | capture_log()
63 |
64 | if set_init and hasattr(algorithm, 'initialization'):
65 | algorithm.initialization = get_init_sampler()
66 |
67 | if hasattr(algorithm, 'repair'):
68 | algorithm.repair = ArchOptRepair()
69 |
70 | if results_folder is not None:
71 | algorithm.callback = ResultsStorageCallback(results_folder, callback=algorithm.callback)
72 |
73 | algorithm.evaluator = ArchOptEvaluator(results_folder=results_folder)
74 |
75 | return algorithm
76 |
77 |
78 | class ArchOptNSGA2(NSGA2):
79 | """NSGA2 preconfigured with mixed-variable operators and other architecture optimization measures"""
80 |
81 | def __init__(self,
82 | pop_size=100,
83 | sampling=HierarchicalSampling(),
84 | repair=ArchOptRepair(),
85 | mating=MixedDiscreteMating(repair=ArchOptRepair(), eliminate_duplicates=LargeDuplicateElimination()),
86 | eliminate_duplicates=LargeDuplicateElimination(),
87 | survival=RankAndCrowdingSurvival(),
88 | output=EHVMultiObjectiveOutput(),
89 | results_folder=None,
90 | **kwargs):
91 |
92 | evaluator = ArchOptEvaluator(results_folder=results_folder)
93 | callback = ResultsStorageCallback(results_folder) if results_folder is not None else None
94 |
95 | super().__init__(pop_size=pop_size, sampling=sampling, repair=repair, mating=mating,
96 | eliminate_duplicates=eliminate_duplicates, survival=survival, output=output,
97 | evaluator=evaluator, callback=callback, **kwargs)
98 |
99 | def _setup(self, problem, **kwargs):
100 | set_global_random_seed(self.seed)
101 |
102 |
103 | def get_nsga2(pop_size: int, results_folder=None, **kwargs):
104 | """Returns a NSGA2 algorithm preconfigured to work with mixed-discrete variables and other architecture optimization
105 | measures"""
106 | capture_log()
107 | return ArchOptNSGA2(pop_size=pop_size, results_folder=results_folder, **kwargs)
108 |
109 |
110 | class DOEAlgorithm(ArchOptNSGA2):
111 | """Algorithm that stops after initialization"""
112 |
113 | def __init__(self, *args, **kwargs):
114 | super().__init__(*args, **kwargs)
115 | self._set_termination()
116 |
117 | self._init_sampling = self.initialization.sampling
118 |
119 | def set_doe_size(self, problem, doe_size: int, **kwargs) -> bool:
120 | """
121 | Set the DOE size, also if the algo is already initialized with a prior population.
122 | Returns whether the DOE size was increased.
123 | """
124 | was_increased = False
125 | self.pop_size = doe_size
126 | self._set_termination()
127 |
128 | # Modify an existing initial population if set
129 | if isinstance(self.initialization.sampling, Population):
130 | init_pop = self.initialization.sampling
131 |
132 | if doe_size < len(init_pop):
133 | log.info(f'Reducing initialized population size from {len(init_pop)} to {doe_size}; '
134 | f'discarding {len(init_pop)-doe_size} points!')
135 | self.initialization.sampling = init_pop[:doe_size]
136 |
137 | elif doe_size > len(init_pop):
138 | n_add = doe_size-len(init_pop)
139 |
140 | log.info(f'Adding {n_add} samples to increase DOE size from {len(init_pop)} to {doe_size}')
141 | add_pop = self._init_sampling(problem, n_add, **kwargs)
142 | add_pop = self.repair(problem, add_pop)
143 |
144 | self.initialization.sampling = Population.merge(init_pop, add_pop)
145 | was_increased = True
146 |
147 | log.info(f'New DOE size: {len(self.initialization.sampling)}')
148 |
149 | return was_increased
150 |
151 | def _set_termination(self):
152 | self.termination = MaximumFunctionCallTermination(n_max_evals=self.pop_size)
153 |
154 | def has_next(self):
155 | return not self.is_initialized
156 |
157 | def _infill(self):
158 | raise RuntimeError('Infill should not be called!')
159 |
160 |
161 | def get_doe_algo(doe_size: int, results_folder=None, **kwargs):
162 | """Returns an algorithm preconfigured for architecture optimization that will only run a DOE. Useful when
163 | evaluations is expensive and more inspection is needed before continuing with optimization"""
164 | capture_log()
165 | return DOEAlgorithm(pop_size=doe_size, results_folder=results_folder, **kwargs)
166 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/botorch_interface/algo.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | import numpy as np
26 | import pymoo.core.variable as var
27 | from pymoo.core.population import Population
28 | from sb_arch_opt.problem import ArchOptProblemBase
29 |
30 | try:
31 | from ax import ParameterType, RangeParameter, ChoiceParameter, SearchSpace, Experiment, OptimizationConfig, \
32 | Objective, MultiObjective, OutcomeConstraint, Metric, ComparisonOp, MultiObjectiveOptimizationConfig, \
33 | Trial, Data
34 | from ax.service.managed_loop import OptimizationLoop
35 | from ax.modelbridge.dispatch_utils import choose_generation_strategy
36 |
37 | HAS_BOTORCH = True
38 | except ImportError:
39 | HAS_BOTORCH = False
40 |
41 | __all__ = ['AxInterface', 'check_dependencies']
42 |
43 |
44 | def check_dependencies():
45 | if not HAS_BOTORCH:
46 | raise ImportError('BoTorch/Ax dependencies not installed: python setup.py install[botorch]')
47 |
48 |
49 | class AxInterface:
50 | """
51 | Class handling interfacing between ArchOptProblemBase and the Ax optimization loop, based on:
52 | https://ax.dev/tutorials/gpei_hartmann_developer.html
53 |
54 | Restart can be implemented based on:
55 | - https://ax.dev/tutorials/generation_strategy.html#3B.-JSON-storage
56 | - https://ax.dev/tutorials/gpei_hartmann_service.html#7.-Save-/-reload-optimization-to-JSON-/-SQL
57 | Failed trails can be marked (as a primitive way of dealing with hidden constraints):
58 | - https://ax.dev/tutorials/gpei_hartmann_service.html#Special-Cases
59 | """
60 |
61 | def __init__(self, problem: ArchOptProblemBase):
62 | check_dependencies()
63 | self._problem = problem
64 |
65 | def get_optimization_loop(self, n_init: int, n_infill: int, seed: int = None) -> 'OptimizationLoop':
66 | experiment = self.get_experiment()
67 | n_eval_total = n_init+n_infill
68 | generation_strategy = choose_generation_strategy(
69 | search_space=experiment.search_space,
70 | experiment=experiment,
71 | num_trials=n_eval_total,
72 | num_initialization_trials=n_init,
73 | max_parallelism_override=self._problem.get_n_batch_evaluate(),
74 | )
75 |
76 | return OptimizationLoop(
77 | experiment=experiment,
78 | evaluation_function=self.evaluate,
79 | total_trials=n_eval_total,
80 | generation_strategy=generation_strategy,
81 | random_seed=seed,
82 | )
83 |
84 | def get_search_space(self) -> 'SearchSpace':
85 | """Gets the search space as defined by the underlying problem"""
86 | parameters = []
87 | for i, var_def in enumerate(self._problem.des_vars):
88 | name = f'x{i}'
89 | if isinstance(var_def, var.Real):
90 | parameters.append(RangeParameter(
91 | name=name, parameter_type=ParameterType.FLOAT, lower=var_def.bounds[0], upper=var_def.bounds[1]))
92 |
93 | elif isinstance(var_def, var.Integer):
94 | parameters.append(RangeParameter(
95 | name=name, parameter_type=ParameterType.INT, lower=var_def.bounds[0], upper=var_def.bounds[1]))
96 |
97 | elif isinstance(var_def, var.Binary):
98 | parameters.append(ChoiceParameter(
99 | name=name, parameter_type=ParameterType.INT, values=[0, 1], is_ordered=True))
100 |
101 | elif isinstance(var_def, var.Choice):
102 | parameters.append(ChoiceParameter(
103 | name=name, parameter_type=ParameterType.INT, values=var_def.options, is_ordered=False))
104 |
105 | else:
106 | raise RuntimeError(f'Unsupported design variable type: {var_def!r}')
107 |
108 | return SearchSpace(parameters)
109 |
110 | def get_optimization_config(self) -> 'OptimizationConfig':
111 | """Gets the optimization config (objectives and constraints) as defined by the underlying problem"""
112 |
113 | if self._problem.n_eq_constr > 0:
114 | raise RuntimeError('Currently equality constraints are not supported!')
115 | constraints = [OutcomeConstraint(Metric(name=f'g{i}'), ComparisonOp.LEQ, bound=0., relative=False)
116 | for i in range(self._problem.n_ieq_constr)]
117 |
118 | if self._problem.n_obj == 1:
119 | return OptimizationConfig(
120 | objective=Objective(Metric(name='f0'), minimize=True),
121 | outcome_constraints=constraints,
122 | )
123 |
124 | objective = MultiObjective(objectives=[
125 | Objective(Metric(name=f'f{i}'), minimize=True) for i in range(self._problem.n_obj)])
126 |
127 | return MultiObjectiveOptimizationConfig(
128 | objective=objective,
129 | outcome_constraints=constraints,
130 | )
131 |
132 | def get_experiment(self) -> 'Experiment':
133 | return Experiment(
134 | name=repr(self._problem),
135 | search_space=self.get_search_space(),
136 | optimization_config=self.get_optimization_config(),
137 | )
138 |
139 | def evaluate(self, parameterization: dict, _=None) -> dict:
140 | x = np.array([[parameterization[f'x{i}'] for i in range(self._problem.n_var)]])
141 | out = self._problem.evaluate(x, return_as_dictionary=True)
142 |
143 | metrics = {}
144 | for i in range(self._problem.n_obj):
145 | metrics[f'f{i}'] = out['F'][0, i]
146 | for i in range(self._problem.n_ieq_constr):
147 | metrics[f'g{i}'] = out['G'][0, i]
148 | return metrics
149 |
150 | def get_population(self, opt_loop: 'OptimizationLoop') -> Population:
151 | x, f, g = [], [], []
152 | data_by_trial = opt_loop.experiment.data_by_trial
153 | trial: 'Trial'
154 | for trial in opt_loop.experiment.trials.values():
155 | x.append([trial.arm.parameters[f'x{i}'] for i in range(self._problem.n_var)])
156 |
157 | data: 'Data' = list(data_by_trial[trial.index].values())[0]
158 | values = data.df.set_index('metric_name')['mean']
159 | f.append([values[f'f{i}'] for i in range(self._problem.n_obj)])
160 | g.append([values[f'g{i}'] for i in range(self._problem.n_ieq_constr)])
161 |
162 | kwargs = {'X': np.array(x), 'F': np.array(f)}
163 | if self._problem.n_ieq_constr > 0:
164 | kwargs['G'] = np.array(g)
165 | return Population.new(**kwargs)
166 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/pymoo_interface/metrics.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | import numpy as np
26 | from pymoo.core.problem import Problem
27 | from pymoo.core.indicator import Indicator
28 | from pymoo.indicators.hv import Hypervolume
29 | from pymoo.core.population import Population
30 | from pymoo.util.display.column import Column
31 | from pymoo.core.termination import TerminateIfAny
32 | from pymoo.util.display.multi import MultiObjectiveOutput
33 | from pymoo.util.normalization import ZeroToOneNormalization
34 | from pymoo.termination.delta import DeltaToleranceTermination
35 | from pymoo.termination.max_gen import MaximumGenerationTermination
36 | from pymoo.termination.max_eval import MaximumFunctionCallTermination
37 | from pymoo.termination.default import DefaultSingleObjectiveTermination
38 |
39 | from sb_arch_opt.problem import ArchOptProblemBase
40 |
41 | __all__ = ['get_default_termination', 'SmoothedIndicator', 'IndicatorDeltaToleranceTermination', 'EstimateHV',
42 | 'DeltaHVTermination', 'EHVMultiObjectiveOutput']
43 |
44 |
45 | def get_default_termination(problem: Problem, xtol=5e-4, cvtol=1e-8, tol=1e-4, n_iter_check=5, n_max_gen=100,
46 | n_max_eval: int = None):
47 | if problem.n_obj == 1:
48 | return DefaultSingleObjectiveTermination(
49 | xtol=xtol, cvtol=cvtol, ftol=tol, period=n_iter_check, n_max_gen=n_max_gen, n_max_evals=n_max_eval)
50 |
51 | return DeltaHVTermination(tol=tol, n_max_gen=n_max_gen, n_max_eval=n_max_eval)
52 | # return DefaultMultiObjectiveTermination(
53 | # xtol=xtol, cvtol=cvtol, ftol=ftol, period=n_iter_check, n_max_gen=n_max_gen, n_max_evals=n_max_eval)
54 |
55 |
56 | class SmoothedIndicator(Indicator):
57 | """Smooths an underlying indicator using an exponential moving average"""
58 |
59 | def __init__(self, indicator: Indicator, n_filter: int = 5):
60 | self.indicator = indicator
61 | self.n_filter = n_filter
62 | super().__init__()
63 | self.data = []
64 |
65 | @property
66 | def alpha(self):
67 | return 1/(1+self.n_filter)
68 |
69 | def _do(self, f, *args, **kwargs):
70 | value = self.indicator.do(f, *args, **kwargs)
71 | if value is None or np.isnan(value):
72 | return np.nan
73 | self.data.append(value)
74 |
75 | weight_factor = 1-self.alpha
76 | next_weight = 1
77 |
78 | ema = self.data[0]
79 | weight = 1.
80 | for value in self.data[1:]:
81 | weight *= weight_factor
82 | if ema != value:
83 | ema = ((weight * ema) + (next_weight * value)) / (weight + next_weight)
84 | weight += next_weight
85 |
86 | return ema
87 |
88 |
89 | class IndicatorDeltaToleranceTermination(DeltaToleranceTermination):
90 | """Delta tolerance termination based on some indicator"""
91 |
92 | def __init__(self, indicator: Indicator, tol, n_skip=0):
93 | self.indicator = indicator
94 | super().__init__(tol, n_skip=n_skip)
95 | self.scale = None
96 |
97 | def _delta(self, prev, current):
98 | delta = np.abs(prev - current)
99 | if np.isnan(delta):
100 | return 100
101 |
102 | # Scale the value to improve percentage calculation
103 | if self.scale is None:
104 | # At the current value, delta-tol should be 100 (--> 1%)
105 | self.scale = (100-self.tol)/(delta-self.tol)
106 |
107 | delta = (delta-self.tol)*self.scale + self.tol
108 | return delta
109 |
110 | def _data(self, algorithm):
111 | f, feas = algorithm.opt.get("F", "feas")
112 | return self.indicator.do(f[feas])
113 |
114 |
115 | class EstimateHV(Hypervolume):
116 | """An indicator for the Hypervolume without knowing the initial reference point"""
117 |
118 | def __init__(self):
119 | super().__init__(ref_point=1, norm_ref_point=False)
120 | self.ref_point = None
121 |
122 | def do(self, f, *args, **kwargs):
123 | if f.ndim == 1:
124 | f = f[None, :]
125 |
126 | if self.ref_point is None:
127 | f_invalid = np.any(np.isinf(f) | np.isnan(f), axis=1)
128 | f_valid = f[~f_invalid, :]
129 | if len(f_valid) == 0:
130 | return np.nan
131 |
132 | nadir = np.max(f[~f_invalid, :], axis=0)
133 | ideal = np.min(f[~f_invalid, :], axis=0)
134 |
135 | self.normalization = ZeroToOneNormalization(ideal, nadir)
136 | self.ref_point = self.normalization.forward(nadir)
137 |
138 | return super().do(f, *args, **kwargs)
139 |
140 |
141 | class DeltaHVTermination(TerminateIfAny):
142 | """
143 | Termination criterion tracking the difference in HV improvement, filtered by an EMA. For more information, see:
144 |
145 | J.H. Bussemaker et al., "Effectiveness of Surrogate-Based Optimization Algorithms for System Architecture
146 | Optimization", AIAA Aviation 2021, DOI: [10.2514/6.2021-3095](https://arc.aiaa.org/doi/10.2514/6.2021-3095)
147 | """
148 |
149 | def __init__(self, tol=1e-4, n_filter=2, n_max_gen=100, n_max_eval: int = None):
150 | termination = [
151 | IndicatorDeltaToleranceTermination(SmoothedIndicator(EstimateHV(), n_filter=n_filter), tol),
152 | MaximumGenerationTermination(n_max_gen=n_max_gen),
153 | ]
154 | if n_max_eval is not None:
155 | termination += [
156 | MaximumFunctionCallTermination(n_max_evals=n_max_eval),
157 | ]
158 | super().__init__(*termination)
159 |
160 |
161 | class EHVMultiObjectiveOutput(MultiObjectiveOutput):
162 | """Multi-objective output that also displays the estimated HV and some population statistics"""
163 |
164 | def __init__(self, pop_stats=True):
165 | super().__init__()
166 | self.ehv_col = Column('hv_est')
167 | self.estimate_hv = EstimateHV()
168 |
169 | self.pop_stat_cols = []
170 | if pop_stats:
171 | self.pop_stat_cols = [Column('not_failed'), Column('feasible'), Column('optimal')]
172 |
173 | def initialize(self, algorithm):
174 | super().initialize(algorithm)
175 | self.columns += [self.ehv_col]+self.pop_stat_cols
176 |
177 | def update(self, algorithm):
178 | super().update(algorithm)
179 |
180 | f, feas = algorithm.opt.get("F", "feas")
181 | f = f[feas]
182 |
183 | self.ehv_col.set(self.estimate_hv.do(f) if len(f) > 0 and f.shape[1] > 0 else None)
184 |
185 | if len(self.pop_stat_cols) > 0:
186 | pop_stats = ArchOptProblemBase.get_population_statistics(
187 | algorithm.pop if algorithm.pop is not None else Population.new())
188 | stats = [f'{row[0]} ({row[1]})' for row in pop_stats.iloc[1:, 1:3].values]
189 | for i, stat in enumerate(stats):
190 | self.pop_stat_cols[i].set(stat)
191 |
--------------------------------------------------------------------------------
/sb_arch_opt/algo/tpe_interface/api.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright: (c) 2023, Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
5 | Contact: jasper.bussemaker@dlr.de
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | """
25 | import logging
26 | import numpy as np
27 | from typing import Optional
28 | from sb_arch_opt.problem import *
29 | from sb_arch_opt.util import capture_log
30 | from sb_arch_opt.algo.pymoo_interface.api import ResultsStorageCallback, ArchOptEvaluator
31 | from sb_arch_opt.algo.pymoo_interface.storage_restart import initialize_from_previous_results
32 | from ConfigSpace import ConfigurationSpace, Float, Integer, Categorical
33 |
34 | import pymoo.core.variable as var
35 | from pymoo.core.algorithm import Algorithm
36 | from pymoo.core.population import Population
37 | from pymoo.util.optimum import filter_optimum
38 | from pymoo.core.initialization import Initialization
39 | from pymoo.util.display.single import SingleObjectiveOutput
40 |
41 | try:
42 | from tpe.optimizer import TPEOptimizer
43 | HAS_TPE = True
44 | except ImportError:
45 | HAS_TPE = False
46 |
47 | __all__ = ['HAS_TPE', 'ArchTPEInterface', 'TPEAlgorithm', 'initialize_from_previous_results']
48 |
49 | log = logging.getLogger('sb_arch_opt.tpe')
50 |
51 |
52 | def check_dependencies():
53 | if not HAS_TPE:
54 | raise RuntimeError('TPE dependencies not installed: pip install -e .[tpe]')
55 |
56 |
57 | class ArchTPEInterface:
58 | """
59 | Class for interfacing the Tree-structured Parzen Estimator (TPE) optimization algorithm. For more info, see:
60 |
61 | Bergstra et al., "Algorithms for Hyper-Parameter Optimization", 2011, available at:
62 | https://papers.nips.cc/paper/2011/file/86e8f7ab32cfd12577bc2619bc635690-Paper.pdf
63 |
64 | Currently only supports single-objective unconstrained problems.
65 | """
66 |
67 | def __init__(self, problem: ArchOptProblemBase):
68 | check_dependencies()
69 | capture_log()
70 |
71 | if problem.n_obj != 1:
72 | raise ValueError('Currently only single-objective problems are supported!')
73 | if problem.n_ieq_constr != 0 or problem.n_eq_constr != 0:
74 | raise ValueError('Currently only unconstrained problems are supported!')
75 |
76 | self._problem = problem
77 | self._optimizer: Optional['TPEOptimizer'] = None
78 |
79 | def initialize(self):
80 | self._optimizer = self._get_optimizer()
81 |
82 | def ask_init(self):
83 | if self._optimizer is None:
84 | self.initialize()
85 | return self._convert_to_x(self._optimizer.initial_sample())
86 |
87 | def ask(self):
88 | if self._optimizer is None:
89 | self.initialize()
90 | return self._convert_to_x(self._optimizer.sample())
91 |
92 | def _convert_to_x(self, config):
93 | is_cat_mask = self._problem.is_cat_mask
94 |
95 | x = []
96 | for ix in range(self._problem.n_var):
97 | key = f'x{ix}'
98 | x.append(int(config[key]) if is_cat_mask[ix] else config[key])
99 | return np.array([x])
100 |
101 | def tell(self, x: np.ndarray, f: float):
102 | assert x.shape == (self._problem.n_var,)
103 | assert self._optimizer is not None
104 |
105 | out_config = {}
106 | is_cat_mask = self._problem.is_cat_mask
107 | for ix in range(self._problem.n_var):
108 | key = f'x{ix}'
109 | out_config[key] = str(int(x[ix])) if is_cat_mask[ix] else x[ix]
110 |
111 | # Report outputs
112 | results = {'f': f}
113 | self._optimizer.update(out_config, results, runtime=0.)
114 |
115 | def optimize(self, n_init: int, n_infill: int):
116 | self.initialize()
117 |
118 | x_results, f_results = [], []
119 | for i_iter in range(n_init+n_infill):
120 | is_init = i_iter < n_init
121 | log.info(f'Iteration {i_iter+1}/{n_init+n_infill} ({"init" if is_init else "infill"})')
122 |
123 | # Get next point to evaluate
124 | x_eval = self.ask_init() if is_init else self.ask()
125 |
126 | # Evaluate
127 | out = self._problem.evaluate(x_eval, return_as_dictionary=True)
128 |
129 | x_out = out['X'][0, :]
130 | f = out['F'][0, 0]
131 | self.tell(x_out, f)
132 |
133 | log.info(f'Evaluated: {f:.3g} @ {x_out}')
134 | x_results.append(x_out)
135 | f_results.append(f)
136 |
137 | x_results, f_results = np.array(x_results), np.array(f_results)
138 | return x_results, f_results
139 |
140 | def _get_optimizer(self):
141 | return TPEOptimizer(
142 | obj_func=lambda *args, **kwargs: None, # We're using the ask-tell interface
143 | config_space=self._get_config_space(),
144 | metric_name='f',
145 | result_keys=['f'],
146 | )
147 |
148 | def _get_config_space(self):
149 | params = {}
150 | for i, dv in enumerate(self._problem.des_vars):
151 | name = f'x{i}'
152 | if isinstance(dv, var.Real):
153 | params[name] = Float(name, bounds=dv.bounds)
154 | elif isinstance(dv, var.Integer):
155 | params[name] = Integer(name, bounds=dv.bounds)
156 | elif isinstance(dv, var.Binary):
157 | params[name] = Integer(name, bounds=(0, 1))
158 | elif isinstance(dv, var.Choice):
159 | params[name] = Categorical(name, items=[str(i) for i in range(len(dv.options))])
160 | else:
161 | raise ValueError(f'Unknown variable type: {dv!r}')
162 |
163 | return ConfigurationSpace(space=params)
164 |
165 |
166 | class TPEInitialization(Initialization):
167 |
168 | def __init__(self):
169 | self.interface: Optional[ArchTPEInterface] = None
170 | super().__init__(sampling=None)
171 |
172 | def do(self, problem, n_samples, **kwargs):
173 | x_init = np.row_stack([self.interface.ask_init() for _ in range(n_samples)])
174 | return Population.new(X=x_init)
175 |
176 |
177 | class TPEAlgorithm(Algorithm):
178 | """
179 | The Tree-structured Parzen Estimator (TPE) optimization algorithm implemented as a pymoo Algorithm.
180 |
181 | Note that through pymoo itself you can also access Optuna's TPE algorithm, however that one does not support design
182 | space hierarchy like SBArchOpt supports it.
183 | """
184 |
185 | def __init__(self, n_init: int, results_folder=None, output=SingleObjectiveOutput(), **kwargs):
186 | self._interface: Optional[ArchTPEInterface] = None
187 | self.n_init = n_init
188 | self.initialization = TPEInitialization()
189 |
190 | evaluator = ArchOptEvaluator(results_folder=results_folder)
191 | callback = ResultsStorageCallback(results_folder) if results_folder is not None else None
192 |
193 | super().__init__(evaluator=evaluator, callback=callback, output=output, **kwargs)
194 |
195 | def _setup(self, problem, **kwargs):
196 | if not isinstance(problem, ArchOptProblemBase):
197 | raise RuntimeError('The TPE algorithm only works with SBArchOpt problem definitions!')
198 |
199 | self._interface = interface = ArchTPEInterface(problem)
200 | interface.initialize()
201 |
202 | if isinstance(self.initialization, TPEInitialization):
203 | self.initialization.interface = self._interface
204 |
205 | def _initialize_infill(self):
206 | return self.initialization.do(self.problem, self.n_init)
207 |
208 | def _infill(self):
209 | return Population.new(X=self._interface.ask())
210 |
211 | def _initialize_advance(self, infills=None, **kwargs):
212 | self._advance(infills, is_init=True, **kwargs)
213 |
214 | def _advance(self, infills=None, is_init=False, **kwargs):
215 | if not is_init:
216 | self.pop = Population.merge(self.pop, infills)
217 | x, f = infills.get('X'), infills.get('F')
218 | for i in range(len(infills)):
219 | self._interface.tell(x[i, :], f[i, 0])
220 |
221 | def _set_optimum(self):
222 | pop = self.pop
223 | if self.opt is not None:
224 | pop = Population.merge(self.opt, pop)
225 | self.opt = filter_optimum(pop, least_infeasible=True)
226 |
--------------------------------------------------------------------------------
/sb_arch_opt/tests/algo/test_segomoe.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import tempfile
3 | import numpy as np
4 | from pymoo.optimize import minimize
5 | from pymoo.core.population import Population
6 | from sb_arch_opt.algo.segomoe_interface import *
7 | from sb_arch_opt.problems.continuous import Branin
8 | from sb_arch_opt.problems.discrete import MDBranin
9 | from sb_arch_opt.problems.md_mo import MOHimmelblau, MDMOHimmelblau
10 | from sb_arch_opt.problems.constrained import ArchCantileveredBeam, MDCantileveredBeam, ArchWeldedBeam, MDWeldedBeam
11 | from sb_arch_opt.problems.hidden_constraints import Mueller01, MOHierarchicalRosenbrockHC
12 |
13 | def check_dependency():
14 | if not HAS_SMT:
15 | return pytest.mark.skipif(not HAS_SMT, reason='SMT dependency not installed')
16 | else:
17 | return pytest.mark.skipif(not HAS_SEGOMOE, reason='SEGOMOE dependencies not installed')
18 |
19 |
20 | @pytest.fixture
21 | def results_folder():
22 | with tempfile.TemporaryDirectory() as tmp_folder:
23 | yield tmp_folder
24 |
25 |
26 | @check_dependency()
27 | def test_interface(results_folder):
28 | interface = SEGOMOEInterface(Branin(), results_folder, n_init=10, n_infill=1)
29 | assert interface.x.shape == (0, 2)
30 | assert interface.n == 0
31 | assert interface.x_failed.shape == (0, 2)
32 | assert interface.n_failed == 0
33 | assert interface.n_tried == 0
34 | assert interface.y.shape == (0, 1)
35 | assert interface.f.shape == (0, 1)
36 | assert interface.g.shape == (0, 0)
37 | assert interface.h.shape == (0, 0)
38 | assert len(interface.pop) == 0
39 | assert len(interface.opt) == 0
40 |
41 |
42 | @check_dependency()
43 | def test_so_cont(results_folder):
44 | interface = SEGOMOEInterface(Branin(), results_folder, n_init=10, n_infill=1)
45 | opt = interface.run_optimization()
46 | assert interface.x.shape == (11, 2)
47 | assert interface.n == 11
48 | assert interface.n_failed == 0
49 | assert interface.n_tried == 11
50 | assert interface.y.shape == (11, 1)
51 | assert interface.f.shape == (11, 1)
52 | assert interface.g.shape == (11, 0)
53 | assert interface.h.shape == (11, 0)
54 | assert len(interface.pop) == 11
55 | assert len(opt) == 1
56 |
57 | interface2 = SEGOMOEInterface(Branin(), results_folder, n_init=10, n_infill=2)
58 | interface2.initialize_from_previous()
59 | assert interface2.x.shape == (11, 2)
60 |
61 | interface2.run_optimization()
62 | assert interface2.x.shape == (12, 2)
63 |
64 |
65 | @check_dependency()
66 | def test_so_cont_constrained(results_folder):
67 | interface = SEGOMOEInterface(ArchCantileveredBeam(), results_folder, n_init=10, n_infill=5, use_moe=False)
68 | opt = interface.run_optimization()
69 | assert interface.f.shape == (15, 1)
70 | assert interface.g.shape == (15, 2)
71 |
72 | feasible_mask = np.all(interface.g < 0, axis=1)
73 | assert len(np.where(feasible_mask)[0]) > 0
74 | assert len(opt) == 1
75 |
76 | pop = interface.pop
77 | assert np.all(pop.get('feas') == feasible_mask)
78 |
79 | interface2 = SEGOMOEInterface(ArchCantileveredBeam(), results_folder, n_init=10, n_infill=6)
80 | interface2.run_optimization()
81 | assert np.all(interface2.g[:-1, :] == interface.g)
82 |
83 |
84 | @check_dependency()
85 | def test_so_mixed(results_folder):
86 | interface = SEGOMOEInterface(MDBranin(), results_folder, n_init=10, n_infill=1)
87 | opt = interface.run_optimization()
88 | assert interface.x.shape == (11, 4)
89 | assert interface.y.shape == (11, 1)
90 | assert interface.f.shape == (11, 1)
91 | assert interface.g.shape == (11, 0)
92 | assert interface.h.shape == (11, 0)
93 | assert len(interface.pop) == 11
94 | assert len(opt) == 1
95 |
96 | assert np.all(interface.x == MDBranin().correct_x(interface.x)[0])
97 |
98 |
99 | @check_dependency()
100 | def test_so_mixed_constrained(results_folder):
101 | interface = SEGOMOEInterface(MDCantileveredBeam(), results_folder, n_init=10, n_infill=2, use_moe=False)
102 | opt = interface.run_optimization()
103 | assert interface.f.shape == (12, 1)
104 | assert interface.g.shape == (12, 2)
105 |
106 | feasible_mask = np.all(interface.g < 0, axis=1)
107 | assert len(np.where(feasible_mask)[0]) > 0
108 | assert len(opt) == 1
109 |
110 | pop = interface.pop
111 | assert np.all(pop.get('feas') == feasible_mask)
112 |
113 |
114 | @check_dependency()
115 | def test_so_failing(results_folder):
116 | interface = SEGOMOEInterface(Mueller01(), results_folder, n_init=50, n_infill=2, use_moe=False)
117 | interface.run_optimization()
118 | assert interface.n < 52
119 | assert interface.n_tried == 52
120 | assert interface.n + interface.n_failed == 52
121 | assert len(interface.pop) == interface.n_tried
122 |
123 | x, x_failed, y = interface._get_xy(interface.pop)
124 | assert x.shape[0] == interface.n
125 | assert x_failed.shape[0] == interface.n_failed
126 | assert y.shape[0] == interface.n
127 |
128 | interface2 = SEGOMOEInterface(Mueller01(), results_folder, n_init=50, n_infill=2, use_moe=False)
129 | interface2.initialize_from_previous()
130 | assert interface2.n < 52
131 | assert interface2.n_tried == 52
132 | assert interface2.n + interface2.n_failed == 52
133 | assert len(interface2.pop) == interface.n_tried
134 |
135 |
136 | @check_dependency()
137 | def test_mo_cont(results_folder):
138 | interface = SEGOMOEInterface(MOHimmelblau(), results_folder, n_init=50, n_infill=1)
139 | opt = interface.run_optimization()
140 | assert interface.x.shape == (51, 2)
141 | assert interface.y.shape == (51, 2)
142 | assert interface.f.shape == (51, 2)
143 | assert interface.g.shape == (51, 0)
144 | assert interface.h.shape == (51, 0)
145 | assert len(interface.pop) == 51
146 | assert len(opt) > 1
147 |
148 |
149 | @check_dependency()
150 | def test_mo_cont_constrained(results_folder):
151 | interface = SEGOMOEInterface(ArchWeldedBeam(), results_folder, n_init=10, n_infill=1)
152 | opt = interface.run_optimization()
153 | assert interface.x.shape == (11, 4)
154 | assert interface.y.shape == (11, 6)
155 | assert interface.f.shape == (11, 2)
156 | assert interface.g.shape == (11, 4)
157 | assert interface.h.shape == (11, 0)
158 | assert len(interface.pop) == 11
159 | assert len(opt) > 1
160 |
161 |
162 | @check_dependency()
163 | def test_mo_mixed(results_folder):
164 | interface = SEGOMOEInterface(MDMOHimmelblau(), results_folder, n_init=10, n_infill=1)
165 | opt = interface.run_optimization()
166 | assert interface.x.shape == (11, 2)
167 | assert interface.y.shape == (11, 2)
168 | assert interface.f.shape == (11, 2)
169 | assert interface.g.shape == (11, 0)
170 | assert interface.h.shape == (11, 0)
171 | assert len(interface.pop) == 11
172 | assert len(opt) >= 1
173 |
174 |
175 | @check_dependency()
176 | def test_mo_mixed_constrained(results_folder):
177 | interface = SEGOMOEInterface(MDWeldedBeam(), results_folder, n_init=10, n_infill=1)
178 | opt = interface.run_optimization()
179 | assert interface.x.shape == (11, 4)
180 | assert interface.y.shape == (11, 6)
181 | assert interface.f.shape == (11, 2)
182 | assert interface.g.shape == (11, 4)
183 | assert interface.h.shape == (11, 0)
184 | assert len(interface.pop) == 11
185 | assert len(opt) >= 1
186 |
187 |
188 | @check_dependency()
189 | def test_mo_failing(results_folder):
190 | interface = SEGOMOEInterface(MOHierarchicalRosenbrockHC(), results_folder, n_init=20, n_infill=1, use_moe=False)
191 | interface.run_optimization()
192 | assert interface.n < 21
193 | assert interface.n_tried == 21
194 | assert interface.n + interface.n_failed == 21
195 | assert len(interface.pop) == interface.n_tried
196 |
197 |
198 | @check_dependency()
199 | def test_ask_tell(results_folder):
200 | problem = Mueller01()
201 | interface = SEGOMOEInterface(problem, results_folder, n_init=50, n_infill=2, use_moe=False)
202 |
203 | while interface.optimization_has_ask():
204 | x = interface.optimization_ask()
205 | pop = Population.new(**problem.evaluate(x, return_as_dictionary=True))
206 | interface.optimization_tell_pop(pop)
207 |
208 | assert interface.n < 52
209 | assert interface.n_tried == 52
210 | assert interface.n + interface.n_failed == 52
211 |
212 |
213 | @check_dependency()
214 | def test_pymoo_algo(results_folder):
215 | problem = Branin()
216 | interface = SEGOMOEInterface(problem, results_folder, n_init=10, n_infill=1, use_moe=False)
217 | algo = SEGOMOEAlgorithm(interface)
218 |
219 | result = minimize(problem, algo)
220 | assert len(result.pop) == 11
221 | assert len(result.opt) == 1
222 |
223 |
224 | @check_dependency()
225 | def test_pymoo_algo_restart(results_folder):
226 | for i in range(2):
227 | problem = Branin()
228 | algo = SEGOMOEAlgorithm(SEGOMOEInterface(problem, results_folder, n_init=10, n_infill=i+1, use_moe=False))
229 | algo.initialize_from_previous_results(problem)
230 |
231 | result = minimize(problem, algo)
232 | assert len(result.pop) == 10+(i+1)
233 |
234 |
235 | @check_dependency()
236 | def test_pymoo_algo_ask_tell(results_folder):
237 | problem = Branin()
238 | algo = SEGOMOEAlgorithm(SEGOMOEInterface(problem, results_folder, n_init=10, n_infill=2, use_moe=False))
239 |
240 | algo.setup(problem)
241 | while algo.has_next():
242 | infills = algo.ask()
243 | pop = Population.new(**problem.evaluate(infills.get('X'), return_as_dictionary=True))
244 | algo.tell(pop)
245 |
246 | result = algo.result()
247 | assert len(result.pop) == 12
248 | assert len(result.opt) == 1
249 |
--------------------------------------------------------------------------------