├── 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 | 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 | ![BoTorch Logo](https://hebo.readthedocs.io/en/latest/_static/hebo.png) 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 | ![BoTorch Logo](https://github.com/pytorch/botorch/raw/main/botorch_logo_lockup.png) 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 | ![pymoo Logo](https://github.com/anyoptimization/pymoo-data/blob/main/logo.png?raw=true) 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 | ![SBArchOpt Logo](https://github.com/jbussemaker/SBArchOpt/blob/main/docs/logo.svg) 2 | 3 | # SBArchOpt: Surrogate-Based Architecture Optimization 4 | 5 | [![Tests](https://github.com/jbussemaker/SBArchOpt/workflows/Tests/badge.svg)](https://github.com/jbussemaker/SBArchOpt/actions/workflows/tests.yml?query=workflow%3ATests) 6 | [![PyPI](https://img.shields.io/pypi/v/sb-arch-opt.svg)](https://pypi.org/project/sb-arch-opt) 7 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) 8 | [![JOSS](https://joss.theoj.org/papers/0b2b765c04d31a4cead77140f82ecba0/status.svg)](https://joss.theoj.org/papers/0b2b765c04d31a4cead77140f82ecba0) 9 | [![Documentation Status](https://readthedocs.org/projects/sbarchopt/badge/?version=latest)](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 | --------------------------------------------------------------------------------