├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples └── egor_optimizer.py ├── openmdao_extensions ├── __init__.py ├── egobox_doe_driver.py ├── egobox_egor_driver.py ├── onera_sego_driver.py ├── openturns_doe_driver.py ├── reckless_nonlinear_block_gs.py ├── salib_doe_driver.py ├── smt_doe_driver.py └── tests │ ├── functions_test.py │ ├── sellar_int │ ├── .wop │ ├── __init__.py │ ├── disc1.py │ ├── disc1_base.py │ ├── disc2.py │ ├── disc2_base.py │ ├── functions.py │ ├── functions_base.py │ ├── mda_init.py │ ├── sellar.py │ └── sellar_base.py │ ├── test_egobox_doe_driver.py │ ├── test_egobox_egor_driver.py │ ├── test_onera_sego_driver.py │ ├── test_openturns_doe_driver.py │ ├── test_reckless_nlbgs.py │ ├── test_salib_doe_driver.py │ └── test_smt_doe_driver.py ├── requirements.txt └── setup.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish to PyPI 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | build_sdist: 10 | name: Build source distribution 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Build sdist 16 | run: pipx run build --sdist 17 | 18 | - uses: actions/upload-artifact@v4 19 | with: 20 | name: openmdao-extensions-src 21 | path: dist/*.tar.gz 22 | 23 | upload_pypi: 24 | needs: [build_sdist] 25 | runs-on: ubuntu-latest 26 | environment: pypi 27 | permissions: 28 | id-token: write 29 | steps: 30 | - uses: actions/download-artifact@v4 31 | with: 32 | pattern: openmdao-extensions-* 33 | merge-multiple: true 34 | path: dist 35 | 36 | - uses: pypa/gh-action-pypi-publish@release/v1 37 | with: 38 | verbose: true 39 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | python-version: ["3.9", "3.10", "3.11"] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | python -m pip install ruff pytest 28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | pip install -e . 30 | - name: Format with ruff 31 | run: | 32 | ruff check . 33 | - name: Test with pytest 34 | run: | 35 | pytest 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | *.sqlite3 58 | *.sqlite 59 | *.out 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | # ignore openmdao reports 109 | **/reports 110 | 111 | # ruff 112 | **/.ruff_cache 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenMDAO Extensions 2 | 3 | [![Tests](https://github.com/OneraHub/openmdao_extensions/workflows/Tests/badge.svg)](https://github.com/OneraHub/openmdao_extensions/actions?query=workflow%3ATests) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/54f346b4094a42f081c4f674e2990aee)](https://app.codacy.com/gh/whatsopt/openmdao_extensions/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 5 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v0.json)](https://github.com/charliermarsh/ruff) 6 | 7 | Set of specialized classes to handle specific methods using OpenMDAO framework. 8 | 9 | * `EgoboxEgorDriver` : an OpenMDAO driver for Egor optimizer from [Egobox](https://github.com/relf/egobox#egobox) library 10 | * `EgoboxDOEDriver` : an OpenMDAO driver for Mixint LHS DoE from [Egobox](https://github.com/relf/egobox#egobox) library 11 | * `OneraSegoDriver` : an OpenMDAO driver for Onera Super EGO optimizers 12 | * `SmtDOEDriver` : an OpenMDAO driver for sampling methods from surrogate [SMT](https://smt.readthedocs.io/en/latest/) library 13 | * `SalibDOEDriver` : an OpenMDAO driver for Morris or Saltelli DoE from sensitive analysis [SALib](https://salib.readthedocs.io/en/latest/) library 14 | * `OpenturnsDoeDriver` : an OpenMDAO driver for DoE following distributions from [OpenTURNS](http://www.openturns.org/) library 15 | * `RecklessNonlinearBlockGS` : a specialized version of the `NonlinearBlockGS` solver to select variables used in the convergence criterion. 16 | 17 | ## Installation 18 | 19 | ```bash 20 | pip install openmdao-extensions 21 | ``` 22 | 23 | ## License 24 | 25 | Licensed under the Apache License, Version 2.0 (the "License"); 26 | you may not use this file except in compliance with the License. 27 | You may obtain a copy of the License at 28 | 29 | 30 | 31 | Unless required by applicable law or agreed to in writing, software 32 | distributed under the License is distributed on an "AS IS" BASIS, 33 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | See the License for the specific language governing permissions and 35 | limitations under the License. 36 | -------------------------------------------------------------------------------- /examples/egor_optimizer.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | from openmdao.test_suite.components.sellar_feature import SellarMDA 3 | from openmdao_extensions.egobox_egor_driver import EgoboxEgorDriver 4 | 5 | import egobox as egx 6 | 7 | # To display Egor optimizer traces 8 | # import logging 9 | # logging.basicConfig(level=logging.INFO) 10 | 11 | prob = om.Problem() 12 | prob.model = SellarMDA() 13 | 14 | prob.model.add_design_var("x", lower=0, upper=10) 15 | prob.model.add_design_var("z", lower=0, upper=10) 16 | prob.model.add_objective("obj") 17 | prob.model.add_constraint("con1", upper=0) 18 | prob.model.add_constraint("con2", upper=0) 19 | 20 | prob.driver = EgoboxEgorDriver() 21 | 22 | # To display available options 23 | # help(egx.Egor) 24 | prob.driver.opt_settings["maxiter"] = 20 25 | prob.driver.opt_settings["infill_strategy"] = egx.InfillStrategy.WB2 26 | prob.driver.opt_settings["infill_optimizer"] = egx.InfillOptimizer.SLSQP 27 | 28 | prob.setup() 29 | prob.set_solver_print(level=0) 30 | 31 | prob.run_driver() 32 | 33 | print("minimum found at") 34 | print(prob.get_val("x")[0]) 35 | print(prob.get_val("z")) 36 | 37 | print("minimum objective") 38 | print(prob.get_val("obj")[0]) 39 | -------------------------------------------------------------------------------- /openmdao_extensions/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.3.2" 2 | -------------------------------------------------------------------------------- /openmdao_extensions/egobox_doe_driver.py: -------------------------------------------------------------------------------- 1 | """ 2 | Driver for running model on design of experiments cases using egobox LHS method 3 | """ 4 | import numpy as np 5 | 6 | from openmdao.api import DOEDriver 7 | from openmdao.drivers.doe_generators import DOEGenerator 8 | 9 | EGOBOX_NOT_INSTALLED = False 10 | try: 11 | import egobox as egx 12 | except ImportError: 13 | EGOBOX_NOT_INSTALLED = True 14 | 15 | 16 | class EgoboxDOEGenerator(DOEGenerator): 17 | def __init__(self, n_cases, **kwargs): 18 | super(EgoboxDOEGenerator, self).__init__(**kwargs) 19 | 20 | if EGOBOX_NOT_INSTALLED: 21 | raise RuntimeError( 22 | "egobox library is not installed. Run `pip install egobox` to do so." 23 | ) 24 | 25 | # number of cases 26 | self.n_cases = n_cases 27 | 28 | def __call__(self, design_vars, model=None): 29 | x_specs = [] 30 | for name, meta in design_vars.items(): 31 | infos = model.get_io_metadata(includes=name) 32 | dvs_int = {} 33 | for absname in infos: 34 | if name == infos[absname]["prom_name"] and ( 35 | infos[absname]["tags"] & {"wop:int"} 36 | ): 37 | dvs_int[name] = egx.XType.INT 38 | 39 | size = meta["size"] 40 | meta_low = meta["lower"] 41 | meta_high = meta["upper"] 42 | 43 | print(name) 44 | for j in range(size): 45 | if isinstance(meta_low, np.ndarray): 46 | p_low = meta_low[j] 47 | else: 48 | p_low = meta_low 49 | 50 | if isinstance(meta_high, np.ndarray): 51 | p_high = meta_high[j] 52 | else: 53 | p_high = meta_high 54 | 55 | if name in dvs_int: 56 | x_specs.append(egx.XSpec(egx.XType.INT, [p_low, p_high])) 57 | else: 58 | x_specs.append(egx.XSpec(egx.XType.FLOAT, [p_low, p_high])) 59 | cases = egx.lhs(x_specs, self.n_cases) 60 | sample = [] 61 | for i in range(cases.shape[0]): 62 | j = 0 63 | for name, meta in design_vars.items(): 64 | size = meta["size"] 65 | sample.append((name, cases[i, j : j + size])) 66 | j += size 67 | yield sample 68 | 69 | 70 | class EgoboxDOEDriver(DOEDriver): 71 | """ 72 | Baseclass for egobox design-of-experiments LHS driver. 73 | """ 74 | 75 | def __init__(self, **kwargs): 76 | super().__init__() 77 | 78 | if EGOBOX_NOT_INSTALLED: 79 | raise RuntimeError( 80 | "egobox library is not installed. Run `pip install egobox` to do so." 81 | ) 82 | 83 | self.options.declare( 84 | "n_cases", types=int, default=2, desc="number of sample to generate" 85 | ) 86 | self.options.update(kwargs) 87 | n_cases = self.options["n_cases"] 88 | self.options["generator"] = EgoboxDOEGenerator(n_cases=n_cases) 89 | 90 | def _set_name(self): 91 | self._name = "egobox_DOE_LHS" 92 | -------------------------------------------------------------------------------- /openmdao_extensions/egobox_egor_driver.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import traceback 3 | 4 | from openmdao.core.driver import Driver, RecordingDebugging 5 | from openmdao.core.analysis_error import AnalysisError 6 | 7 | EGOBOX_NOT_INSTALLED = False 8 | try: 9 | import egobox as egx 10 | from egobox import Egor 11 | except ImportError: 12 | EGOBOX_NOT_INSTALLED = True 13 | 14 | 15 | def to_list(lst, size): 16 | if not (isinstance(lst, np.ndarray) or isinstance(lst, list)): 17 | return [lst] * size 18 | diff_len = len(lst) - size 19 | if diff_len > 0: 20 | return lst[0:size] 21 | elif diff_len < 0: 22 | return [lst[0]] * size 23 | else: 24 | return lst 25 | 26 | 27 | class EgoboxEgorDriver(Driver): 28 | """OpenMDAO driver for egobox optimizer""" 29 | 30 | def __init__(self, **kwargs): 31 | """Initialize the driver with the given options.""" 32 | super(EgoboxEgorDriver, self).__init__(**kwargs) 33 | 34 | if EGOBOX_NOT_INSTALLED: 35 | raise RuntimeError("egobox library is not installed.") 36 | 37 | # What we support 38 | self.supports["optimization"] = True 39 | self.supports["inequality_constraints"] = True 40 | self.supports["linear_constraints"] = True 41 | self.supports["integer_design_vars"] = True 42 | 43 | # What we don't support 44 | self.supports["equality_constraints"] = False 45 | self.supports["two_sided_constraints"] = False 46 | self.supports["multiple_objectives"] = False 47 | self.supports["active_set"] = False 48 | self.supports["simultaneous_derivatives"] = False 49 | self.supports["total_jac_sparsity"] = False 50 | self.supports["gradients"] = False 51 | self.supports._read_only = True 52 | 53 | self.opt_settings = {} 54 | 55 | def _declare_options(self): 56 | self.options.declare( 57 | "optimizer", 58 | default="EGOR", 59 | values=["EGOR"], 60 | desc="Name of optimizer to use", 61 | ) 62 | 63 | def _setup_driver(self, problem): 64 | super(EgoboxEgorDriver, self)._setup_driver(problem) 65 | 66 | self.comm = None 67 | 68 | def run(self): 69 | model = self._problem().model 70 | 71 | self.iter_count = 0 72 | self.name = f"egobox_optimizer_{self.options['optimizer'].lower()}" 73 | 74 | # Initial Run 75 | with RecordingDebugging(self.name, self.iter_count, self) as rec: 76 | # Initial Run 77 | model.run_solve_nonlinear() 78 | rec.abs = 0.0 79 | rec.rel = 0.0 80 | self.iter_count += 1 81 | 82 | # Format design variables to suit segomoe implementation 83 | self.xspecs = self._initialize_vars(model) 84 | 85 | # Format constraints to suit segomoe implementation 86 | self.n_cstr = self._initialize_cons() 87 | 88 | # Format option dictionary to suit Egor implementation 89 | cstr_tol = [1e-4] * self.n_cstr if self.n_cstr else [] 90 | optim_settings = { 91 | "cstr_tol": cstr_tol, 92 | } 93 | n_iter = self.opt_settings["maxiter"] 94 | optim_settings.update( 95 | {k: v for k, v in self.opt_settings.items() if k != "maxiter"} 96 | ) 97 | 98 | dim = 0 99 | for name, meta in self._designvars.items(): 100 | dim += meta["size"] 101 | if dim > 10: 102 | self.optim_settings["kpls_dim"] = 3 103 | 104 | # Instanciate a SEGO optimizer 105 | egor = Egor( 106 | xspecs=self.xspecs, 107 | n_cstr=self.n_cstr, 108 | **optim_settings, 109 | ) 110 | 111 | # Run the optim 112 | res = egor.minimize(self._objfunc, max_iters=n_iter) 113 | 114 | # Set optimal parameters 115 | i = 0 116 | for name, meta in self._designvars.items(): 117 | size = meta["size"] 118 | self.set_design_var(name, res.x_opt[i : i + size]) 119 | i += size 120 | 121 | with RecordingDebugging(self.name, self.iter_count, self) as rec: 122 | model.run_solve_nonlinear() 123 | rec.abs = 0.0 124 | rec.rel = 0.0 125 | self.iter_count += 1 126 | 127 | return True 128 | 129 | def _initialize_vars(self, model): 130 | dvs_int = {} 131 | for name, meta in self._designvars.items(): 132 | infos = model.get_io_metadata(includes=name) 133 | for absname in infos: 134 | if name == infos[absname]["prom_name"] and ( 135 | infos[absname]["tags"] & {"wop:int"} 136 | ): 137 | dvs_int[name] = egx.XType(egx.XType.INT) 138 | 139 | variables = [] 140 | desvars = self._designvars 141 | for name, meta in desvars.items(): 142 | vartype = dvs_int.get(name, egx.XType.FLOAT) 143 | if meta["size"] > 1: 144 | if np.isscalar(meta["lower"]): 145 | variables += [ 146 | egx.XSpec(vartype, [meta["lower"], meta["upper"]]) 147 | for i in range(meta["size"]) 148 | ] 149 | else: 150 | variables += [ 151 | egx.XSpec(vartype, [meta["lower"], meta["upper"]]) 152 | for i in range(meta["size"]) 153 | ] 154 | else: 155 | variables += [egx.XSpec(vartype, [meta["lower"], meta["upper"]])] 156 | return variables 157 | 158 | def _initialize_cons(self, eq_tol=None, ieq_tol=None): 159 | """Format OpenMDAO constraints to suit EGOR implementation 160 | 161 | Parameters 162 | ---------- 163 | eq_tol: dict 164 | Dictionary to define specific tolerance for eq constraints 165 | {'[groupName]': [tol]} Default tol = 1e-5 166 | """ 167 | con_meta = self._cons 168 | 169 | self.ieq_cons = { 170 | name: con for name, con in con_meta.items() if not con["equals"] 171 | } 172 | 173 | # Inequality constraints 174 | n_cstr = 0 175 | for name in self.ieq_cons.keys(): 176 | meta = con_meta[name] 177 | size = meta["size"] 178 | # Bounds - double sided is supported 179 | lower = to_list(meta["lower"], size) 180 | upper = to_list(meta["upper"], size) 181 | for k in range(size): 182 | if (lower[k] is None or lower[k] < -1e29) and upper[k] == 0.0: 183 | n_cstr += 1 184 | else: 185 | raise ValueError( 186 | f"Constraint {lower[k]} < g(x) < {upper[k]} not handled by Egor driver" 187 | ) 188 | return n_cstr 189 | 190 | def _objfunc(self, points): 191 | """ 192 | Function that evaluates and returns the objective function and the 193 | constraints. This function is called by SEGOMOE 194 | 195 | Parameters 196 | ---------- 197 | point : numpy.ndarray 198 | point to evaluate 199 | 200 | Returns 201 | ------- 202 | func_dict : dict 203 | Dictionary of all functional variables evaluated at design point. 204 | fail : int 205 | 0 for successful function evaluation 206 | 1 for unsuccessful function evaluation 207 | """ 208 | res = np.zeros((points.shape[0], 1 + self.n_cstr)) 209 | model = self._problem().model 210 | 211 | for k, point in enumerate(points): 212 | try: 213 | # Pass in new parameters 214 | i = 0 215 | 216 | for name, meta in self._designvars.items(): 217 | size = meta["size"] 218 | self.set_design_var(name, point[i : i + size]) 219 | i += size 220 | 221 | # Execute the model 222 | with RecordingDebugging( 223 | self.options["optimizer"], self.iter_count, self 224 | ) as _: 225 | self.iter_count += 1 226 | try: 227 | model.run_solve_nonlinear() 228 | 229 | # Let the optimizer try to handle the error 230 | except AnalysisError: 231 | model._clear_iprint() 232 | 233 | # Get the objective function evaluation - single obj support 234 | for obj in self.get_objective_values().values(): 235 | res[k, 0] = obj 236 | 237 | # Get the constraint evaluations 238 | j = 1 239 | for con_res in self.get_constraint_values().values(): 240 | # Make sure con_res is array_like 241 | con_res = to_list(con_res, 1) 242 | # Perform mapping 243 | for i, _ in enumerate(con_res): 244 | res[k, j + i] = con_res[i] 245 | j += 1 246 | 247 | except Exception as msg: 248 | tb = traceback.format_exc() 249 | print("Exception: %s" % str(msg)) 250 | print(70 * "=", tb, 70 * "=") 251 | 252 | return res 253 | -------------------------------------------------------------------------------- /openmdao_extensions/onera_sego_driver.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import traceback 3 | 4 | from openmdao.core.driver import Driver, RecordingDebugging 5 | from openmdao.core.analysis_error import AnalysisError 6 | 7 | ONERASEGO_NOT_INSTALLED = False 8 | try: 9 | from segomoe.sego_defs import get_sego_options, ExitStatus 10 | from segomoe.constraint import Constraint 11 | from segomoe.sego import Sego 12 | except ImportError: 13 | ONERASEGO_NOT_INSTALLED = True 14 | 15 | 16 | def to_list(lst, size): 17 | if not (isinstance(lst, np.ndarray) or isinstance(lst, list)): 18 | return [lst] * size 19 | diff_len = len(lst) - size 20 | if diff_len > 0: 21 | return lst[0:size] 22 | elif diff_len < 0: 23 | return [lst[0]] * size 24 | else: 25 | return lst 26 | 27 | 28 | class OneraSegoDriver(Driver): 29 | """ 30 | OpenMDAO driver for ONERA SEGOMOE optimizer 31 | """ 32 | 33 | def __init__(self, **kwargs): 34 | """ 35 | Initialize the driver with the following option. 36 | """ 37 | super(OneraSegoDriver, self).__init__(**kwargs) 38 | 39 | if ONERASEGO_NOT_INSTALLED: 40 | raise RuntimeError("Onera SEGOMOE library is not installed.") 41 | 42 | # What we support 43 | self.supports["optimization"] = True 44 | self.supports["inequality_constraints"] = True 45 | self.supports["equality_constraints"] = True 46 | self.supports["two_sided_constraints"] = True 47 | self.supports["linear_constraints"] = True 48 | 49 | # What we don't support 50 | self.supports["multiple_objectives"] = False 51 | self.supports["active_set"] = False 52 | self.supports["integer_design_vars"] = False 53 | self.supports["simultaneous_derivatives"] = False 54 | self.supports["total_jac_sparsity"] = False 55 | self.supports["gradients"] = False 56 | self.supports._read_only = True 57 | 58 | self.opt_settings = {} 59 | 60 | def _declare_options(self): 61 | self.options.declare( 62 | "optimizer", 63 | default="SEGOMOE", 64 | values=["SEGOMOE"], 65 | desc="Name of optimizers to use", 66 | ) 67 | 68 | def _setup_driver(self, problem): 69 | super(OneraSegoDriver, self)._setup_driver(problem) 70 | 71 | self.comm = None 72 | 73 | def run(self, path_hs="", eq_tol={}, ieq_tol={}): 74 | """ 75 | Optimize the problem using SEGOMOE. 76 | 77 | Parameters 78 | ---------- 79 | problem : Problem object 80 | An optimization problem of OpenMDAO framework 81 | path_hs: string, optional 82 | path to a directory storing hot_start data 83 | eq_tol: dict 84 | Dictionary to define specific tolerance for eq constraints 85 | {'[groupName]': [tol]} Default tol = 1e-5 86 | ieq_tol: dict 87 | Dictionary to define specific tolerance for ieq constraints 88 | {'[groupName]': [tol]} Default tol = 1e-5 89 | """ 90 | model = self._problem().model 91 | 92 | path_hs = "" 93 | self.eq_tol = eq_tol 94 | self.ieq_tol = ieq_tol 95 | self.iter_count = 0 96 | self.name = f"onera_optimizer_{self.options['optimizer'].lower()}" 97 | self._sego_vars = [] 98 | self._sego_cons = [] 99 | self._map_con = {} 100 | self._n_mapped_con = 0 101 | 102 | # Initial Run 103 | with RecordingDebugging( 104 | self.options["optimizer"], self.iter_count, self 105 | ) as rec: 106 | # Initial Run 107 | model.run_solve_nonlinear() 108 | rec.abs = 0.0 109 | rec.rel = 0.0 110 | self.iter_count += 1 111 | 112 | # Format design variables to suit segomoe implementation 113 | self._initialize_vars() 114 | 115 | # Format constraints to suit segomoe implementation 116 | self._initialize_cons() 117 | 118 | # Format option dictionary to suit SEGO implementation 119 | optim_settings = {} 120 | for opt, opt_dict in get_sego_options().items(): 121 | optim_settings[opt] = opt_dict["default"] 122 | n_iter = self.opt_settings["maxiter"] 123 | optim_settings.update( 124 | {k: v for k, v in self.opt_settings.items() if k != "maxiter"} 125 | ) 126 | 127 | # In OpenMDAO context, obj and constraints are always evaluated together 128 | optim_settings["grouped_eval"] = True 129 | 130 | # default model 131 | mod_obj = {"corr": "squar_exp", "regr": "constant", "normalize": True} 132 | 133 | dim = 0 134 | for name, meta in self._designvars.items(): 135 | dim += meta["size"] 136 | print("Designvars dimension: ", dim) 137 | if dim > 10: 138 | n_components = 3 139 | mod_obj["n_components"] = n_components 140 | mod_obj["name"] = "KPLS" if dim > 20 else "KPLSK" 141 | else: 142 | n_components = dim 143 | mod_obj["name"] = "KRG" 144 | mod_obj["theta0"] = [1.0] * n_components 145 | mod_obj["thetaL"] = [0.1] * n_components 146 | mod_obj["thetaU"] = [10.0] * n_components 147 | 148 | optim_settings["model_type"] = {"obj": mod_obj, "con": mod_obj} 149 | 150 | print(optim_settings) 151 | # Instanciate a SEGO optimizer 152 | sego = Sego( 153 | self._objfunc, 154 | self._sego_vars, 155 | const=self._sego_cons, 156 | optim_settings=optim_settings, 157 | path_hs=path_hs, 158 | comm=self.comm, 159 | ) 160 | 161 | # Run the optim 162 | # exit_flag, x_best, obj_best, dt_opt = sego.run_optim( 163 | exit_flag, x_best, _, _ = sego.run_optim(n_iter=n_iter) 164 | 165 | # Set optimal parameters 166 | i = 0 167 | for name, meta in self._designvars.items(): 168 | size = meta["size"] 169 | self.set_design_var(name, x_best[0][i : i + size]) 170 | i += size 171 | 172 | with RecordingDebugging( 173 | self.options["optimizer"], self.iter_count, self 174 | ) as rec: 175 | model.run_solve_nonlinear() 176 | rec.abs = 0.0 177 | rec.rel = 0.0 178 | self.iter_count += 1 179 | 180 | self.exit_flag = (exit_flag == ExitStatus.valid_sol) or ( 181 | exit_flag == ExitStatus.solution_reached 182 | ) 183 | 184 | return self.exit_flag 185 | 186 | def _initialize_vars(self): 187 | variables = [] 188 | param_meta = self._designvars 189 | for name, meta in param_meta.items(): 190 | if meta["size"] > 1: 191 | if np.isscalar(meta["lower"]): 192 | variables += [ 193 | { 194 | "name": name + "_" + str(i), 195 | "lb": meta["lower"], 196 | "ub": meta["upper"], 197 | } 198 | for i in range(meta["size"]) 199 | ] 200 | else: 201 | variables += [ 202 | { 203 | "name": name + "_" + str(i), 204 | "lb": meta["lower"][i], 205 | "ub": meta["lower"][i], 206 | } 207 | for i in range(meta["size"]) 208 | ] 209 | else: 210 | variables += [{"name": name, "lb": meta["lower"], "ub": meta["upper"]}] 211 | self._sego_vars = variables 212 | 213 | def _initialize_cons(self, eq_tol=None, ieq_tol=None): 214 | """ 215 | Format OpenMDAO constraints to suit SEGOMOE implementation 216 | 217 | Parameters 218 | ---------- 219 | eq_tol: dict 220 | Dictionary to define specific tolerance for eq constraints 221 | {'[groupName]': [tol]} Default tol = 1e-5 222 | ieq_tol: dict 223 | Dictionary to define specific tolerance for ieq constraints 224 | {'[groupName]': [tol]} Default tol = 1e-5 225 | """ 226 | 227 | # for short 228 | add_con = self._sego_cons.append 229 | 230 | # Start at 1 -> obj first 231 | eval_index = 1 232 | 233 | con_meta = self._cons 234 | eq_cons = {name: con for name, con in con_meta.items() if con["equals"]} 235 | ieq_cons = {name: con for name, con in con_meta.items() if not con["equals"]} 236 | 237 | # Equality constraints 238 | for name in eq_cons.keys(): 239 | meta = con_meta[name] 240 | size = meta["size"] 241 | equals = to_list(meta["equals"], size) 242 | tol = to_list(eq_tol[name] if name in self.eq_tol else 1e-4, size) 243 | 244 | for k in range(size): 245 | add_con( 246 | Constraint("=", equals[k], name=(name + "_" + str(k)), tol=tol[k]) 247 | ) 248 | # Map eq constraints - range to allow double-sided constraint support 249 | self._map_con[name] = range(eval_index, eval_index + size) 250 | eval_index += size 251 | self._n_mapped_con += size 252 | 253 | # Inequality constraints 254 | for name in ieq_cons.keys(): 255 | meta = con_meta[name] 256 | size = meta["size"] 257 | # Bounds - double sided is supported 258 | lower = to_list(meta["lower"], size) 259 | upper = to_list(meta["upper"], size) 260 | tol = to_list(ieq_tol[name] if name in self.ieq_tol else 1e-4, size) 261 | # Index list is updated for each real constraint to handle double sided constraints 262 | index_list = [] 263 | 264 | for k in range(size): 265 | if lower[k] is None or lower[k] < -1e29: 266 | # g(x) < upper 267 | add_con( 268 | Constraint( 269 | "<", upper[k], name=(name + "_" + str(k)), tol=tol[k] 270 | ) 271 | ) 272 | index_list.append(eval_index) 273 | eval_index += 1 274 | self._n_mapped_con += 1 275 | elif upper[k] is None or upper[k] > 1e29: 276 | # g(x) > lower 277 | add_con( 278 | Constraint( 279 | ">", lower[k], name=(name + "_" + str(k)), tol=tol[k] 280 | ) 281 | ) 282 | index_list.append(eval_index) 283 | eval_index += 1 284 | self._n_mapped_con += 1 285 | else: 286 | # lower < g(x) < upper 287 | # Split is currently required for sego 288 | add_con( 289 | Constraint( 290 | "<", upper[k], name=(name + "_" + str(k) + "<"), tol=tol[k] 291 | ) 292 | ) 293 | add_con( 294 | Constraint( 295 | ">", lower[k], name=(name + "_" + str(k) + ">"), tol=tol[k] 296 | ) 297 | ) 298 | index_list.append([eval_index, eval_index + 1]) 299 | eval_index += 2 300 | self._n_mapped_con += 2 301 | self._map_con[name] = index_list 302 | 303 | def _objfunc(self, point): 304 | """ 305 | Function that evaluates and returns the objective function and the 306 | constraints. This function is called by SEGOMOE 307 | 308 | Parameters 309 | ---------- 310 | point : numpy.ndarray 311 | point to evaluate 312 | 313 | Returns 314 | ------- 315 | func_dict : dict 316 | Dictionary of all functional variables evaluated at design point. 317 | fail : int 318 | 0 for successful function evaluation 319 | 1 for unsuccessful function evaluation 320 | """ 321 | fail = False 322 | res = np.zeros(1 + self._n_mapped_con) 323 | model = self._problem().model 324 | 325 | try: 326 | # Pass in new parameters 327 | i = 0 328 | 329 | for name, meta in self._designvars.items(): 330 | size = meta["size"] 331 | self.set_design_var(name, point[i : i + size]) 332 | i += size 333 | 334 | # Execute the model 335 | with RecordingDebugging( 336 | self.options["optimizer"], self.iter_count, self 337 | ) as _: 338 | self.iter_count += 1 339 | try: 340 | model._solve_nonlinear() 341 | 342 | # Let the optimizer try to handle the error 343 | except AnalysisError: 344 | model._clear_iprint() 345 | fail = True 346 | 347 | # Get the objective function evaluation - single obj support 348 | for name, obj in self.get_objective_values().items(): 349 | res[0] = obj 350 | 351 | # Get the constraint evaluations: 352 | for name, con_res in self.get_constraint_values().items(): 353 | # Make sure con_res is array_like 354 | con_res = to_list(con_res, len(self._map_con[name])) 355 | # Perform mapping 356 | for i, con_index in enumerate(self._map_con[name]): 357 | if isinstance(con_index, list): 358 | # Double sided inequality constraint -> duplicate response 359 | for k in con_index: 360 | res[k] = con_res[i] 361 | else: 362 | res[con_index] = con_res[i] 363 | 364 | except Exception as msg: 365 | tb = traceback.format_exc() 366 | 367 | # Exceptions seem to be swallowed by the C code, so this 368 | # should give the user more info than the dreaded "segfault" 369 | print("Exception: %s" % str(msg)) 370 | print(70 * "=", tb, 70 * "=") 371 | fail = True 372 | 373 | return res, fail 374 | -------------------------------------------------------------------------------- /openmdao_extensions/openturns_doe_driver.py: -------------------------------------------------------------------------------- 1 | """ 2 | Driver for running model on design of experiments cases using OpenTURNS sampling methods 3 | """ 4 | import numpy as np 5 | 6 | from openmdao.api import DOEDriver 7 | from openmdao.drivers.doe_generators import DOEGenerator 8 | 9 | OPENTURNS_NOT_INSTALLED = False 10 | try: 11 | import openturns as ot 12 | except ImportError: 13 | OPENTURNS_NOT_INSTALLED = True 14 | 15 | 16 | class OpenturnsMonteCarloDOEGenerator(DOEGenerator): 17 | LIMIT = 1e12 18 | 19 | def __init__(self, n_samples=10, dist=None): 20 | super(OpenturnsMonteCarloDOEGenerator, self).__init__() 21 | 22 | self.n_samples = n_samples 23 | self.distribution = dist 24 | self.called = False 25 | 26 | def __call__(self, uncertain_vars, model=None): 27 | if self.distribution is None: 28 | dists = [] 29 | for name, meta in uncertain_vars.items(): 30 | size = meta["size"] 31 | meta_low = meta["lower"] 32 | meta_high = meta["upper"] 33 | for j in range(size): 34 | if isinstance(meta_low, np.ndarray): 35 | p_low = meta_low[j] 36 | else: 37 | p_low = meta_low 38 | p_low = max(p_low, -self.LIMIT) 39 | 40 | if isinstance(meta_high, np.ndarray): 41 | p_high = meta_high[j] 42 | else: 43 | p_high = meta_high 44 | p_high = min(p_high, self.LIMIT) 45 | 46 | dists.append(ot.Uniform(p_low, p_high)) 47 | self.distribution = ot.ComposedDistribution(dists) 48 | else: 49 | size = 0 50 | for name, meta in uncertain_vars.items(): 51 | size += meta["size"] 52 | if (size) != (self.distribution.getDimension()): 53 | raise RuntimeError( 54 | "Bad distribution dimension: should be equal to uncertain variables size {} " 55 | ", got {}".format(size, self.distribution.getDimension()) 56 | ) 57 | samples = self.distribution.getSample(self.n_samples) 58 | self._cases = np.array(samples) 59 | self.called = True 60 | sample = [] 61 | for i in range(self._cases.shape[0]): 62 | j = 0 63 | for name, meta in uncertain_vars.items(): 64 | size = meta["size"] 65 | sample.append((name, self._cases[i, j : j + size])) 66 | j += size 67 | yield sample 68 | 69 | def get_cases(self): 70 | if not self.called: 71 | raise RuntimeError("Have to run the driver before getting cases") 72 | return self._cases 73 | 74 | 75 | class OpenturnsDOEDriver(DOEDriver): 76 | """ 77 | Baseclass for OpenTURNS design-of-experiments Drivers 78 | """ 79 | 80 | def __init__(self, **kwargs): 81 | super(OpenturnsDOEDriver, self).__init__() 82 | 83 | self.options.declare( 84 | "distribution", 85 | types=ot.ComposedDistribution, 86 | default=None, 87 | allow_none=True, 88 | desc="Joint distribution of uncertain variables", 89 | ) 90 | self.options.declare( 91 | "n_samples", types=int, default=2, desc="number of sample to generate" 92 | ) 93 | self.options.update(kwargs) 94 | 95 | self.options["generator"] = OpenturnsMonteCarloDOEGenerator( 96 | self.options["n_samples"], self.options["distribution"] 97 | ) 98 | 99 | def _set_name(self): 100 | self._name = "OpenTURNS_DOE_MonteCarlo" 101 | 102 | def get_cases(self): 103 | return self.options["generator"].get_cases() 104 | -------------------------------------------------------------------------------- /openmdao_extensions/reckless_nonlinear_block_gs.py: -------------------------------------------------------------------------------- 1 | """Define the NonlinearBlockGS class.""" 2 | 3 | import os 4 | import numpy as np 5 | 6 | from openmdao.core.analysis_error import AnalysisError 7 | from openmdao.recorders.recording_iteration_stack import Recording 8 | from openmdao.api import NonlinearBlockGS 9 | from openmdao import __version__ as openmdao_version 10 | 11 | 12 | class RecklessNonlinearBlockGS(NonlinearBlockGS): 13 | """ 14 | Extends Nonlinear block Gauss-Seidel solver with convergence variables options. 15 | Those options allows to focus on a subset of variables to drive the convergence. 16 | It allows to get quickest convergence by ignoring 'noise' coming highly non linear 17 | variables. Obviously the user has to know what he/she is doing because 18 | in that case some of the variables may not be converged properly 19 | (hence the 'reckless' prefix in the name). 20 | 21 | Attributes 22 | ---------- 23 | _convrg_vars: list of string 24 | List of absolute variable names used to compute relative error and control 25 | solver convergence. 26 | _convrg_rtols: list of float 27 | List of relative error tolerance values for each variables of _convrg_vars. If not set, rtol 28 | value is used for all specified variables. Only used if _convrg_vars is set. 29 | """ 30 | 31 | SOLVER = "NL: RNLBGS" 32 | 33 | def __init__(self, **kwargs): 34 | """ 35 | Initialize all attributes. 36 | 37 | Parameters 38 | ---------- 39 | **kwargs : dict 40 | options dictionary. 41 | """ 42 | super(RecklessNonlinearBlockGS, self).__init__(**kwargs) 43 | 44 | self._convrg_vars = None 45 | self._convrg_rtols = None 46 | 47 | def _declare_options(self): 48 | """ 49 | Declare options before kwargs are processed in the init method. 50 | """ 51 | super(RecklessNonlinearBlockGS, self)._declare_options() 52 | self.options.declare( 53 | "convrg_vars", 54 | types=list, 55 | default=[], 56 | desc="list of variables (names) used by relative error criterium.", 57 | ) 58 | self.options.declare( 59 | "convrg_rtols", 60 | types=list, 61 | default=[], 62 | desc="list of relative error tolerances corresponding to each" 63 | " variable specified in convrg_vars option (rtol is used otherwise)", 64 | ) 65 | 66 | def _solve(self): 67 | """ 68 | Run the iterative solver. 69 | 70 | Overrides opendmao/solvers/solver.py to implement _is_rtol_converged 71 | """ 72 | maxiter = self.options["maxiter"] 73 | atol = self.options["atol"] 74 | iprint = self.options["iprint"] 75 | 76 | self._mpi_print_header() 77 | 78 | self._iter_count = 0 79 | self._iter_initialize() 80 | 81 | if self._convrg_vars: 82 | # get initial value of convrg_vars 83 | norm, val_convrg_vars_minus1 = self._iter_get_norm() 84 | ratio = 1 85 | else: 86 | norm = self._iter_get_norm() 87 | norm0 = norm if norm != 0.0 else 1.0 88 | ratio = norm / norm0 89 | 90 | self._mpi_print(self._iter_count, norm, ratio) 91 | is_rtol_converged = self._is_rtol_converged(ratio) 92 | 93 | while self._iter_count < maxiter and norm > atol and not is_rtol_converged: 94 | with Recording(type(self).__name__, self._iter_count, self) as rec: 95 | self._single_iteration() 96 | self._iter_count += 1 97 | self._run_apply() 98 | 99 | if self._convrg_vars: 100 | norm, val_convrg_vars = self._iter_get_norm() 101 | rec.abs = norm 102 | rec.rel = ( 103 | np.abs(val_convrg_vars - val_convrg_vars_minus1) 104 | / val_convrg_vars_minus1 105 | ) 106 | ratio = ( 107 | np.abs(val_convrg_vars - val_convrg_vars_minus1) 108 | / val_convrg_vars_minus1 109 | ) 110 | ratio = np.max(ratio) 111 | 112 | val_convrg_vars_minus1 = val_convrg_vars 113 | else: 114 | norm = self._iter_get_norm() 115 | # With solvers, we want to record the norm AFTER the call, but the call needs to 116 | # be wrapped in the with for stack purposes, so we locally assign norm & norm0 117 | # into the class. 118 | rec.abs = norm 119 | rec.rel = norm / norm0 120 | ratio = norm / norm0 121 | if norm0 == 0: 122 | norm0 = 1 123 | 124 | self._mpi_print(self._iter_count, norm, ratio) 125 | is_rtol_converged = self._is_rtol_converged(ratio) 126 | 127 | system = self._system() 128 | if system.comm.rank == 0 or os.environ.get("USE_PROC_FILES"): 129 | prefix = self._solver_info.prefix + self.SOLVER 130 | is_rtol_converged = self._is_rtol_converged(ratio) 131 | # Solver terminated early because a Nan in the norm doesn't satisfy the while-loop 132 | # conditionals. 133 | if np.isinf(norm) or np.isnan(norm): 134 | msg = ( 135 | "Solver '{}' on system '{}': residuals contain 'inf' or 'NaN' after {} " 136 | + "iterations." 137 | ) 138 | if iprint > -1: 139 | print( 140 | prefix 141 | + msg.format(self.SOLVER, system.pathname, self._iter_count) 142 | ) 143 | 144 | # Raise AnalysisError if requested. 145 | if self.options["err_on_non_converge"]: 146 | raise AnalysisError( 147 | msg.format(self.SOLVER, system.pathname, self._iter_count) 148 | ) 149 | 150 | # Solver hit maxiter without meeting desired tolerances. 151 | elif norm > atol and not is_rtol_converged: 152 | msg = "Solver '{}' on system '{}' failed to converge in {} iterations." 153 | 154 | if iprint > -1: 155 | print( 156 | prefix 157 | + msg.format(self.SOLVER, system.pathname, self._iter_count) 158 | ) 159 | 160 | # Raise AnalysisError if requested. 161 | if self.options["err_on_non_converge"]: 162 | raise AnalysisError( 163 | msg.format(self.SOLVER, system.pathname, self._iter_count) 164 | ) 165 | 166 | # Solver converged 167 | elif iprint == 1: 168 | print(prefix + " Converged in {} iterations".format(self._iter_count)) 169 | elif iprint == 2: 170 | print(prefix + " Converged") 171 | 172 | def _iter_initialize(self): 173 | """ 174 | Perform any necessary pre-processing operations. 175 | 176 | Returns 177 | ------- 178 | float 179 | initial error. 180 | float 181 | error at the first iteration. 182 | """ 183 | self._convrg_vars = self.options["convrg_vars"] 184 | if self._convrg_vars and not self.options["convrg_rtols"]: 185 | rtol = self.options["rtol"] 186 | self._convrg_rtols = rtol * np.ones(len(self._convrg_vars)) 187 | else: 188 | self._convrg_rtols = self.options["convrg_rtols"] 189 | if len(self._convrg_rtols) != len(self._convrg_vars): 190 | raise RuntimeError( 191 | "Convergence rtols bad size : should be {}, found {}.".format( 192 | len(self._convrg_vars), len(self._convrg_rtols) 193 | ) 194 | ) 195 | 196 | return super(RecklessNonlinearBlockGS, self)._iter_initialize() 197 | 198 | def _is_rtol_converged(self, ratio): 199 | """ 200 | Check convergence regarding relative error tolerance. 201 | 202 | Parameters 203 | ---------- 204 | norm : float 205 | error (residuals norm) 206 | norm0 : float 207 | initial error 208 | 209 | Returns 210 | ------- 211 | bool 212 | whether convergence is reached regarding relative error tolerance 213 | """ 214 | system = self._system() 215 | if self._convrg_vars: 216 | nbvars = len(self._convrg_vars) 217 | rerrs = np.ones(nbvars) 218 | outputs = np.ones(nbvars) 219 | for i, name in enumerate(self._convrg_vars): 220 | outputs = self._get_views_array(system._outputs._views[name]) 221 | residual = self._get_views_array(system._residuals._views[name]) 222 | rerrs[i] = np.linalg.norm(residual) / np.linalg.norm(outputs) 223 | is_rtol_converged = (rerrs < self._convrg_rtols).all() 224 | is_rtol_converged = ratio < self.options["rtol"] 225 | else: 226 | is_rtol_converged = ratio < self.options["rtol"] 227 | return is_rtol_converged 228 | 229 | def _iter_get_norm(self): 230 | """ 231 | Return the norm of the residual regarding convergence variable settings. 232 | 233 | Returns 234 | ------- 235 | float 236 | norm. 237 | """ 238 | system = self._system() 239 | if self._convrg_vars: 240 | total = [] 241 | val_convrg_vars = np.zeros(len(self._convrg_vars)) 242 | for i, name in enumerate(self._convrg_vars): 243 | total.append(system._residuals._views_flat[name]) 244 | val_convrg_vars[i] = np.linalg.norm( 245 | self._get_views_array(system._outputs._views[name]) 246 | ) 247 | norm = np.linalg.norm(np.concatenate(total)) 248 | else: 249 | norm = super(RecklessNonlinearBlockGS, self)._iter_get_norm() 250 | 251 | if self._convrg_vars: 252 | return norm, val_convrg_vars 253 | else: 254 | return norm 255 | 256 | @staticmethod 257 | def _get_views_array(vector_views): 258 | """Simple backward compatibility function as views change 259 | from np.array to (np.array, bool) in openmdao 3.38""" 260 | if openmdao_version > "3.37.0": 261 | return vector_views[0] 262 | else: 263 | return vector_views 264 | -------------------------------------------------------------------------------- /openmdao_extensions/salib_doe_driver.py: -------------------------------------------------------------------------------- 1 | """ 2 | Driver for running model on design of experiments cases using Salib sampling methods 3 | """ 4 | import numpy as np 5 | 6 | from openmdao.api import DOEDriver, OptionsDictionary 7 | from openmdao.drivers.doe_generators import DOEGenerator 8 | 9 | SALIB_NOT_INSTALLED = False 10 | try: 11 | from SALib.sample import morris as ms 12 | from SALib.sample import saltelli 13 | except ImportError: 14 | SALIB_NOT_INSTALLED = True 15 | 16 | 17 | class SalibDOEGenerator(DOEGenerator): 18 | def __init__(self): 19 | if SALIB_NOT_INSTALLED: 20 | raise RuntimeError( 21 | "SALib library is not installed. \ 22 | cf. https://salib.readthedocs.io/en/latest/getting-started.html" 23 | ) 24 | self._cases = np.array([]) 25 | self._pb = None 26 | self.called = False 27 | 28 | def __call__(self, design_vars, model=None): 29 | bounds = [] 30 | names = [] 31 | for name, meta in design_vars.items(): 32 | size = meta["size"] 33 | meta_low = meta["lower"] 34 | meta_high = meta["upper"] 35 | for j in range(size): 36 | if isinstance(meta_low, np.ndarray): 37 | p_low = meta_low[j] 38 | else: 39 | p_low = meta_low 40 | 41 | if isinstance(meta_high, np.ndarray): 42 | p_high = meta_high[j] 43 | else: 44 | p_high = meta_high 45 | 46 | display_name = name.split(".")[-1] 47 | if size > 1: 48 | display_name += str(j) 49 | names.append(display_name) 50 | bounds.append((p_low, p_high)) 51 | 52 | self._pb = { 53 | "num_vars": len(names), 54 | "names": names, 55 | "bounds": bounds, 56 | "groups": None, 57 | } 58 | self._compute_cases() 59 | self.called = True 60 | sample = [] 61 | for i in range(self._cases.shape[0]): 62 | j = 0 63 | for name, meta in design_vars.items(): 64 | size = meta["size"] 65 | sample.append((name, self._cases[i, j : j + size])) 66 | j += size 67 | yield sample 68 | 69 | def _compute_cases(self): 70 | raise RuntimeError("Have to be implemented in subclass.") 71 | 72 | def get_cases(self): 73 | if not self.called: 74 | raise RuntimeError("Have to run the driver before getting cases") 75 | return self._cases 76 | 77 | def get_salib_problem(self): 78 | if not self.called: 79 | raise RuntimeError( 80 | "Have to run the driver before getting the SALib problem" 81 | ) 82 | return self._pb 83 | 84 | 85 | class SalibMorrisDOEGenerator(SalibDOEGenerator): 86 | def __init__(self, n_trajs=2, n_levels=4): 87 | super(SalibMorrisDOEGenerator, self).__init__() 88 | # number of trajectories to apply morris method 89 | self.n_trajs = n_trajs 90 | # number of grid levels 91 | self.n_levels = n_levels 92 | 93 | def _compute_cases(self): 94 | self._cases = ms.sample(self._pb, self.n_trajs, self.n_levels) 95 | 96 | 97 | class SalibSobolDOEGenerator(SalibDOEGenerator): 98 | def __init__(self, n_samples=1000, calc_second_order=True): 99 | super(SalibSobolDOEGenerator, self).__init__() 100 | # number of samples to generate 101 | self.n_samples = n_samples 102 | # whether calculing second order indices 103 | self.calc_second_order = calc_second_order 104 | 105 | def _compute_cases(self): 106 | self._cases = saltelli.sample(self._pb, self.n_samples, self.calc_second_order) 107 | 108 | 109 | class SalibDOEDriver(DOEDriver): 110 | """ 111 | Baseclass for SALib design-of-experiments Drivers 112 | """ 113 | 114 | def __init__(self, **kwargs): 115 | super(SalibDOEDriver, self).__init__() 116 | 117 | if SALIB_NOT_INSTALLED: 118 | raise RuntimeError( 119 | "SALib library is not installed. \ 120 | cf. https://salib.readthedocs.io/en/latest/getting-started.html" 121 | ) 122 | 123 | self.options.declare( 124 | "sa_method_name", 125 | default="Morris", 126 | values=["Morris", "Sobol"], 127 | desc="either Morris or Sobol", 128 | ) 129 | self.options.declare( 130 | "sa_doe_options", 131 | types=dict, 132 | default={}, 133 | desc="options for given SMT sensitivity analysis method", 134 | ) 135 | self.options.update(kwargs) 136 | 137 | self.sa_settings = OptionsDictionary() 138 | if self.options["sa_method_name"] == "Morris": 139 | self.sa_settings.declare( 140 | "n_trajs", 141 | types=int, 142 | default=2, 143 | desc="number of trajectories to apply morris method", 144 | ) 145 | self.sa_settings.declare( 146 | "n_levels", types=int, default=4, desc="number of grid levels" 147 | ) 148 | self.sa_settings.update(self.options["sa_doe_options"]) 149 | n_trajs = self.sa_settings["n_trajs"] 150 | n_levels = self.sa_settings["n_levels"] 151 | self.options["generator"] = SalibMorrisDOEGenerator(n_trajs, n_levels) 152 | elif self.options["sa_method_name"] == "Sobol": 153 | self.sa_settings.declare( 154 | "n_samples", 155 | types=int, 156 | default=500, 157 | desc="number of samples to generate", 158 | ) 159 | self.sa_settings.declare( 160 | "calc_second_order", 161 | types=bool, 162 | default=True, 163 | desc="calculate second-order sensitivities ", 164 | ) 165 | self.sa_settings.update(self.options["sa_doe_options"]) 166 | n_samples = self.sa_settings["n_samples"] 167 | calc_snd = self.sa_settings["calc_second_order"] 168 | self.options["generator"] = SalibSobolDOEGenerator(n_samples, calc_snd) 169 | else: 170 | raise RuntimeError( 171 | "Bad sensitivity analysis method name '{}'".format( 172 | self.options["sa_method_name"] 173 | ) 174 | ) 175 | 176 | def _set_name(self): 177 | self._name = "SALib_DOE_" + self.options["sa_method_name"] 178 | 179 | def get_cases(self): 180 | return self.options["generator"].get_cases() 181 | 182 | def get_salib_problem(self): 183 | return self.options["generator"].get_salib_problem() 184 | -------------------------------------------------------------------------------- /openmdao_extensions/smt_doe_driver.py: -------------------------------------------------------------------------------- 1 | """ 2 | Driver for running model on design of experiments cases using SMT sampling methods 3 | """ 4 | import numpy as np 5 | 6 | from openmdao.api import DOEDriver 7 | from openmdao.drivers.doe_generators import DOEGenerator 8 | 9 | SMT_NOT_INSTALLED = False 10 | try: 11 | from smt.sampling_methods import FullFactorial, LHS, Random 12 | 13 | _SAMPLING_METHODS = {"FullFactorial": FullFactorial, "LHS": LHS, "Random": Random} 14 | 15 | except ImportError: 16 | SMT_NOT_INSTALLED = True 17 | 18 | 19 | class SmtDOEGenerator(DOEGenerator): 20 | def __init__(self, sampling_method_name, n_cases, **kwargs): 21 | super(SmtDOEGenerator, self).__init__() 22 | 23 | if SMT_NOT_INSTALLED: 24 | raise RuntimeError( 25 | "SMT library is not installed. Run `pip install smt`. See https://smt.readthedocs.io/en/latest" 26 | ) 27 | 28 | # number of trajectories to apply morris method 29 | self.sampling_method_name = sampling_method_name 30 | # number of grid levels 31 | self.n_cases = n_cases 32 | # options 33 | self.sampling_method_opts = kwargs 34 | 35 | def __call__(self, design_vars, model=None): 36 | xlimits = [] 37 | for name, meta in design_vars.items(): 38 | size = meta["size"] 39 | meta_low = meta["lower"] 40 | meta_high = meta["upper"] 41 | for j in range(size): 42 | if isinstance(meta_low, np.ndarray): 43 | p_low = meta_low[j] 44 | else: 45 | p_low = meta_low 46 | 47 | if isinstance(meta_high, np.ndarray): 48 | p_high = meta_high[j] 49 | else: 50 | p_high = meta_high 51 | 52 | xlimits.append((p_low, p_high)) 53 | 54 | sampling = _SAMPLING_METHODS[self.sampling_method_name]( 55 | xlimits=np.array(xlimits), **(self.sampling_method_opts) 56 | ) 57 | cases = sampling(self.n_cases) 58 | sample = [] 59 | for i in range(cases.shape[0]): 60 | j = 0 61 | for name, meta in design_vars.items(): 62 | size = meta["size"] 63 | sample.append((name, cases[i, j : j + size])) 64 | j += size 65 | yield sample 66 | 67 | 68 | class SmtDOEDriver(DOEDriver): 69 | """ 70 | Baseclass for SMT design-of-experiments Drivers. 71 | """ 72 | 73 | def __init__(self, **kwargs): 74 | super(SmtDOEDriver, self).__init__() 75 | 76 | if SMT_NOT_INSTALLED: 77 | raise RuntimeError( 78 | "SMT library is not installed. cf. https://https://smt.readthedocs.io/en/latest" 79 | ) 80 | 81 | self.options.declare( 82 | "sampling_method_name", 83 | types=str, 84 | default="LHS", 85 | desc="either LHS, FullFactorial or Ramdom", 86 | ) 87 | self.options.declare( 88 | "n_cases", types=int, default=2, desc="number of sample to generate" 89 | ) 90 | self.options.declare( 91 | "sampling_method_options", 92 | types=dict, 93 | default={}, 94 | desc="options for given SMT sampling method", 95 | ) 96 | 97 | self.options.update(kwargs) 98 | name = self.options["sampling_method_name"] 99 | n_cases = self.options["n_cases"] 100 | opts = self.options["sampling_method_options"] 101 | self.options["generator"] = SmtDOEGenerator( 102 | sampling_method_name=name, n_cases=n_cases, **opts 103 | ) 104 | 105 | def _set_name(self): 106 | self._name = "SMT_DOE_" + self.options["sampling_method_name"] 107 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/functions_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from math import cos, sin, pi 3 | from openmdao.api import ( 4 | IndepVarComp, 5 | Group, 6 | ExplicitComponent, 7 | ) 8 | 9 | 10 | class Branin(ExplicitComponent): 11 | def setup(self): 12 | self.add_input("x1", val=1.0) 13 | self.add_input("x2", val=1.0) 14 | self.add_output("obj", val=1.0) 15 | self.add_output("con", val=1.0) 16 | 17 | @staticmethod 18 | def compute(inputs, outputs): 19 | x_1 = inputs["x1"][0] 20 | x_2 = inputs["x2"][0] 21 | # obj 22 | part1 = (x_2 - (5.1 * x_1**2) / (4.0 * pi**2) + 5.0 * x_1 / pi - 6.0) ** 2 23 | part2 = 10.0 * ((1.0 - 1.0 / (8.0 * pi)) * cos(x_1) + 1.0) 24 | part3 = (5.0 * x_1 + 25.0) / 15.0 25 | outputs["obj"] = part1 + part2 + part3 26 | 27 | # con 28 | x_g1 = (x_1 - 2.5) / 7.5 29 | x_g2 = (x_2 - 7.5) / 7.5 30 | part1 = (4.0 - 2.1 * x_g1**2 + (x_g1**4) / 3.0) * x_g1**2 31 | part2 = x_g1 * x_g2 32 | part3 = (4.0 * x_g2**2 - 4.0) * x_g2**2 33 | part4 = 3.0 * sin(6.0 * (1.0 - x_g1)) 34 | part5 = 3.0 * sin(6.0 * (1.0 - x_g2)) 35 | outputs["con"] = -(part1 + part2 + part3 + part4 + part5 - 6.0) 36 | 37 | 38 | class BraninMDA(Group): 39 | def setup(self): 40 | indeps = self.add_subsystem("indeps", IndepVarComp(), promotes=["*"]) 41 | indeps.add_output("x1", 9.1) 42 | indeps.add_output("x2", 4.75) 43 | 44 | self.add_subsystem("Branin", Branin(), promotes=["*"]) 45 | 46 | 47 | class Ackley(ExplicitComponent): 48 | def setup(self): 49 | self.add_input("x", val=[1.0, 1.0]) 50 | self.add_output("obj", val=1.0) 51 | 52 | @staticmethod 53 | def compute(inputs, outputs): 54 | dim = 2 55 | a = 20.0 56 | b = 0.2 57 | c = 2 * np.pi 58 | point = inputs["x"] 59 | outputs["obj"] = ( 60 | -a * np.exp(-b * np.sqrt(1.0 / dim * np.sum(point**2))) 61 | - np.exp(1.0 / dim * np.sum(np.cos(c * point))) 62 | + a 63 | + np.exp(1) 64 | ) 65 | 66 | 67 | class AckleyMDA(Group): 68 | def setup(self): 69 | indeps = self.add_subsystem("indeps", IndepVarComp(), promotes=["*"]) 70 | indeps.add_output("x", [1.0, 1.0]) 71 | 72 | self.add_subsystem("Ackley", Ackley(), promotes=["*"]) 73 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/.wop: -------------------------------------------------------------------------------- 1 | # This file contains recorded state from wop pull/update commands 2 | # DO NOT EDIT unless you know what you are doing 3 | # Version history: 4 | # * version 2: use toml format, add wop_format_version 5 | # * version 1: initial format "key: val" 6 | # * version 0: no wop file 7 | # 8 | wop_format_version = 2 9 | whatsopt_url = "http://localhost:3000" 10 | analysis_id = 4 11 | framework = "openmdao" 12 | pull_mode = "plain" 13 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | __init__.py generated by WhatsOpt 1.25.1 3 | """ 4 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/disc1.py: -------------------------------------------------------------------------------- 1 | """ 2 | disc1.py generated by WhatsOpt 1.25.1 3 | """ 4 | from .disc1_base import Disc1Base 5 | 6 | 7 | class Disc1(Disc1Base): 8 | """A class to encapsulate Disc1 discipline""" 9 | 10 | def compute(self, inputs, outputs): 11 | """Disc1 computation""" 12 | # Here one can implement discipline resolution code 13 | # (python function or module, external software calls...) 14 | 15 | x = inputs["x"] # shape: 1, type: Integer 16 | y2 = inputs["y2"] # shape: 1, type: Float 17 | z = inputs["z"] # shape: (2,), type: Float 18 | 19 | outputs["y1"] = z[0] ** 2 + z[1] + x - 0.2 * y2 20 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/disc1_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | disc1_base.py generated by WhatsOpt 1.25.1 3 | """ 4 | # DO NOT EDIT unless you know what you are doing 5 | # whatsopt_url: http://localhost:3000 6 | # analysis_id: 4 7 | 8 | 9 | import openmdao.api as om 10 | 11 | 12 | class Disc1Base(om.ExplicitComponent): 13 | """An OpenMDAO base component to encapsulate Disc1 discipline. 14 | This class defines inputs and outputs of the discipline and declare partials. 15 | """ 16 | 17 | def setup(self): 18 | self.add_input("x", val=2, desc="", tags=["wop:int"]) 19 | self.add_input("y2", val=1.0, desc="") 20 | self.add_input("z", val=[5, 2], desc="") 21 | 22 | self.add_output("y1", val=1.0, desc="") 23 | 24 | def setup_partials(self): 25 | self.declare_partials("*", "*", method="fd") 26 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/disc2.py: -------------------------------------------------------------------------------- 1 | """ 2 | disc2.py generated by WhatsOpt 1.25.1 3 | """ 4 | from .disc2_base import Disc2Base 5 | 6 | 7 | class Disc2(Disc2Base): 8 | """A class to encapsulate Disc2 discipline""" 9 | 10 | def compute(self, inputs, outputs): 11 | """Disc2 computation""" 12 | # Here one can implement discipline resolution code 13 | # (python function or module, external software calls...) 14 | 15 | y1 = inputs["y1"] # shape: 1, type: Float 16 | z = inputs["z"] # shape: (2,), type: Float 17 | 18 | if y1.real < 0.0: 19 | y1 *= -1 20 | 21 | outputs["y2"] = y1**0.5 + z[0] + z[1] 22 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/disc2_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | disc2_base.py generated by WhatsOpt 1.25.1 3 | """ 4 | # DO NOT EDIT unless you know what you are doing 5 | # whatsopt_url: http://localhost:3000 6 | # analysis_id: 4 7 | 8 | 9 | import openmdao.api as om 10 | 11 | 12 | class Disc2Base(om.ExplicitComponent): 13 | """An OpenMDAO base component to encapsulate Disc2 discipline. 14 | This class defines inputs and outputs of the discipline and declare partials. 15 | """ 16 | 17 | def setup(self): 18 | self.add_input("y1", val=1.0, desc="") 19 | self.add_input("z", val=[5, 2], desc="") 20 | 21 | self.add_output("y2", val=1.0, desc="") 22 | 23 | def setup_partials(self): 24 | self.declare_partials("*", "*", method="fd") 25 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | functions.py generated by WhatsOpt 1.25.1 3 | """ 4 | from .functions_base import FunctionsBase 5 | from math import exp 6 | 7 | 8 | class Functions(FunctionsBase): 9 | """A class to encapsulate Functions discipline""" 10 | 11 | def compute(self, inputs, outputs): 12 | """Functions computation""" 13 | # Here one can implement discipline resolution code 14 | # (python function or module, external software calls...) 15 | 16 | x = inputs["x"] # shape: 1, type: Integer 17 | y1 = inputs["y1"] # shape: 1, type: Float 18 | y2 = inputs["y2"] # shape: 1, type: Float 19 | z = inputs["z"] # shape: (2,), type: Float 20 | 21 | outputs["f"] = x**2 + z[1] + y1 + exp(-y2) 22 | outputs["g1"] = 3.16 - y1 23 | outputs["g2"] = y2 - 24.0 24 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/functions_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | functions_base.py generated by WhatsOpt 1.25.1 3 | """ 4 | # DO NOT EDIT unless you know what you are doing 5 | # whatsopt_url: http://localhost:3000 6 | # analysis_id: 4 7 | 8 | 9 | import openmdao.api as om 10 | 11 | 12 | class FunctionsBase(om.ExplicitComponent): 13 | """An OpenMDAO base component to encapsulate Functions discipline. 14 | This class defines inputs and outputs of the discipline and declare partials. 15 | """ 16 | 17 | def setup(self): 18 | self.add_input("x", val=2, desc="", tags=["wop:int"]) 19 | self.add_input("y1", val=1.0, desc="") 20 | self.add_input("y2", val=1.0, desc="") 21 | self.add_input("z", val=[5, 2], desc="") 22 | 23 | self.add_output("f", val=1.0, desc="") 24 | self.add_output("g1", val=1.0, desc="") 25 | self.add_output("g2", val=1.0, desc="") 26 | 27 | def setup_partials(self): 28 | self.declare_partials("*", "*", method="fd") 29 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/mda_init.py: -------------------------------------------------------------------------------- 1 | """ 2 | mda_init.py generated by WhatsOpt 1.25.1 3 | """ 4 | 5 | 6 | def initialize(mda): 7 | mda["x"] = 2 8 | mda["z"] = [5, 2] 9 | return mda 10 | 11 | 12 | if __name__ == "__main__": 13 | from tabulate import tabulate 14 | 15 | mda = {} 16 | initialize(mda) 17 | headers = ["parameter", "value"] 18 | data = [[k, v] for k, v in mda.items()] 19 | print(tabulate(data, headers)) 20 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/sellar.py: -------------------------------------------------------------------------------- 1 | """ 2 | sellar.py generated by WhatsOpt 1.25.1 3 | """ 4 | from optparse import OptionParser 5 | import openmdao.api as om 6 | 7 | # from openmdao_extensions.reckless_nonlinear_block_gs import RecklessNonlinearBlockGS 8 | from .sellar_base import SellarBase, SellarFactoryBase 9 | 10 | 11 | class SellarFactory(SellarFactoryBase): 12 | """A factory to create disciplines of Sellar analysis.""" 13 | 14 | # One can override here create methods from the base class to take control over disciplines creation 15 | # then pass an instance of this class as the 'factory' argument of the constructor (see run_*.py scripts), 16 | # example: my_analysis = Sellar(factory=SellarFactory()) 17 | 18 | 19 | class Sellar(SellarBase): 20 | """An OpenMDAO Group to encapsulate Sellar analysis 21 | see https://openmdao.org/newdocs/versions/latest/features/core_features/working_with_groups/main.html 22 | """ 23 | 24 | # One can override here default behaviour provided by WhatsOpt in the base class. 25 | 26 | # def initialize(): 27 | # super().initialize() 28 | # Here one can adjust component initialization 29 | 30 | # Example adding options 31 | # see https://openmdao.org/newdocs/versions/latest/features/core_features/working_with_components/options.html 32 | # 33 | # self.options.declare("n_engines", types=int) 34 | 35 | # Example of manual solver adjusments (imports should be adjusted accordingly) 36 | # see https://openmdao.org/newdocs/versions/latest/features/core_features/controlling_solver_behavior/main.html 37 | # 38 | # self.nonlinear_solver = NewtonSolver() 39 | # self.nonlinear_solver.options['maxiter'] = 20 40 | # self.linear_solver = DirectSolver() 41 | 42 | # def setup(self): 43 | # super().setup() 44 | # 45 | # As an example, here if needs be, one can add code to adjust 46 | # setup (done in parent base class) regarding options defined in initialize 47 | 48 | 49 | if __name__ == "__main__": 50 | parser = OptionParser() 51 | parser.add_option( 52 | "-n", 53 | "--no-n2", 54 | action="store_false", 55 | dest="n2_view", 56 | help="display N2 openmdao viewer", 57 | ) 58 | (options, args) = parser.parse_args() 59 | 60 | problem = om.Problem() 61 | problem.model = Sellar() 62 | 63 | problem.setup() 64 | problem.final_setup() 65 | 66 | if options.n2_view: 67 | from openmdao.visualization.n2_viewer.n2_viewer import n2 68 | 69 | n2(problem) 70 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/sellar_int/sellar_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | sellar_base.py generated by WhatsOpt 1.25.1 3 | """ 4 | # DO NOT EDIT unless you know what you are doing 5 | # whatsopt_url: http://localhost:3000 6 | # analysis_id: 4 7 | 8 | 9 | 10 | import openmdao.api as om 11 | from openmdao.api import NonlinearBlockGS 12 | from openmdao.api import ScipyKrylov 13 | 14 | 15 | from .disc1 import Disc1 16 | from .disc2 import Disc2 17 | from .functions import Functions 18 | 19 | 20 | class SellarFactoryBase: 21 | """ 22 | A factory for all plain disciplines of Sellar analysis. 23 | 24 | One can override methods in a subclass to take control over disciplines creation 25 | and pass that subclass to the analysis constructor as a factory argument. 26 | """ 27 | 28 | def create_disc1(self): 29 | return Disc1() 30 | 31 | def create_disc2(self): 32 | return Disc2() 33 | 34 | def create_functions(self): 35 | return Functions() 36 | 37 | 38 | class SellarBase(om.Group): 39 | """An OpenMDAO base component to encapsulate Sellar MDA""" 40 | 41 | def initialize(self): 42 | self.options.declare( 43 | "factory", default=SellarFactoryBase(), types=object, recordable=False 44 | ) 45 | 46 | self.nonlinear_solver = NonlinearBlockGS() 47 | self.nonlinear_solver.options["atol"] = 1.0e-08 48 | self.nonlinear_solver.options["rtol"] = 1.0e-08 49 | self.nonlinear_solver.options["err_on_non_converge"] = False 50 | self.nonlinear_solver.options["maxiter"] = 10 51 | self.nonlinear_solver.options["iprint"] = 1 52 | 53 | self.linear_solver = ScipyKrylov() 54 | self.linear_solver.options["atol"] = 1.0e-08 55 | self.linear_solver.options["rtol"] = 1.0e-08 56 | self.linear_solver.options["err_on_non_converge"] = True 57 | self.linear_solver.options["maxiter"] = 10 58 | 59 | self.linear_solver.options["iprint"] = 1 60 | 61 | def setup(self): 62 | self.set_input_defaults("x", val=2) 63 | self.set_input_defaults("z", val=[5, 2]) 64 | self.add_subsystem( 65 | "disc1", self.create_disc1(), promotes=["x", "y1", "y2", "z"] 66 | ) 67 | self.add_subsystem("disc2", self.create_disc2(), promotes=["y1", "y2", "z"]) 68 | self.add_subsystem( 69 | "functions", 70 | self.create_functions(), 71 | promotes=["f", "g1", "g2", "x", "y1", "y2", "z"], 72 | ) 73 | 74 | def create_disc1(self): 75 | return self.options["factory"].create_disc1() 76 | 77 | def create_disc2(self): 78 | return self.options["factory"].create_disc2() 79 | 80 | def create_functions(self): 81 | return self.options["factory"].create_functions() 82 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/test_egobox_doe_driver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import openmdao.api as om 4 | from sellar_int.sellar import Sellar 5 | from openmdao_extensions.egobox_doe_driver import EgoboxDOEDriver 6 | from openmdao_extensions.egobox_doe_driver import EGOBOX_NOT_INSTALLED 7 | 8 | 9 | class TestEgoboxDoeDriver(unittest.TestCase): 10 | def assert_case_generation(self, n, driver): 11 | pb = om.Problem(Sellar()) 12 | 13 | pb.model.add_design_var("x", lower=0, upper=10) 14 | pb.model.add_design_var("z", lower=0, upper=10) 15 | pb.driver = driver 16 | pb.setup() 17 | 18 | case_recorder_filename = "{}/test_egobox_doe_driver_{}.sqlite".format( 19 | pb.get_outputs_dir(), n 20 | ) 21 | 22 | recorder = om.SqliteRecorder(case_recorder_filename) 23 | pb.driver.add_recorder(recorder) 24 | 25 | pb.run_driver() 26 | pb.cleanup() 27 | 28 | reader = om.CaseReader(case_recorder_filename) 29 | cases = reader.list_cases("driver") 30 | os.remove(case_recorder_filename) 31 | self.assertEqual(len(cases), n) 32 | for i in range(len(cases)): 33 | case = reader.get_case(cases[i]) 34 | print(case) 35 | 36 | @unittest.skipIf(EGOBOX_NOT_INSTALLED, "EGOBOX library is not installed") 37 | def test_egobox_lhs_doe_driver(self): 38 | n = 10 39 | self.assert_case_generation( 40 | n, 41 | EgoboxDOEDriver(n_cases=n), 42 | ) 43 | 44 | 45 | if __name__ == "__main__": 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/test_egobox_egor_driver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import openmdao.api as om 4 | from openmdao.test_suite.components.sellar_feature import SellarMDA 5 | from openmdao_extensions.egobox_egor_driver import EgoboxEgorDriver 6 | from openmdao_extensions.egobox_egor_driver import EGOBOX_NOT_INSTALLED 7 | 8 | from openmdao_extensions.tests.functions_test import BraninMDA, AckleyMDA 9 | 10 | 11 | class TestEgor(unittest.TestCase): 12 | def setUp(self): 13 | pass 14 | 15 | def tearDown(self): 16 | pass 17 | 18 | @unittest.skipIf(EGOBOX_NOT_INSTALLED, "egobox is not installed") 19 | def test_sellar(self): 20 | self.pb = pb = om.Problem(SellarMDA()) 21 | pb.model.add_design_var("x", lower=0, upper=10) 22 | pb.model.add_design_var("z", lower=0, upper=10) 23 | pb.model.add_objective("obj") 24 | pb.model.add_constraint("con1", upper=0) 25 | pb.model.add_constraint("con2", upper=0) 26 | pb.driver = EgoboxEgorDriver(optimizer="EGOR") 27 | pb.driver.opt_settings["maxiter"] = 10 28 | pb.setup() 29 | self.case_recorder_filename = "{}/test_egobox_driver_sellar.sqlite".format( 30 | pb.get_outputs_dir() 31 | ) 32 | recorder = om.SqliteRecorder(self.case_recorder_filename) 33 | pb.model.add_recorder(recorder) 34 | self.pb.run_driver() 35 | self.assertTrue(os.path.exists(self.case_recorder_filename)) 36 | reader = om.CaseReader(self.case_recorder_filename) 37 | for case_id in reader.list_cases(): 38 | case = reader.get_case(case_id) 39 | print(case.outputs["obj"]) 40 | 41 | @unittest.skipIf(EGOBOX_NOT_INSTALLED, "egobox is not installed") 42 | def test_sellar_int(self): 43 | self.pb = pb = om.Problem(SellarMDA()) 44 | pb.model.add_design_var("x", lower=0, upper=10) 45 | pb.model.add_design_var("z", lower=0, upper=10) 46 | pb.model.add_objective("obj") 47 | pb.model.add_constraint("con1", upper=0) 48 | pb.model.add_constraint("con2", upper=0) 49 | pb.driver = EgoboxEgorDriver(optimizer="EGOR") 50 | pb.driver.opt_settings["maxiter"] = 10 51 | 52 | pb.setup() 53 | self.case_recorder_filename = "{}/test_egobox_driver_sellar_int.sqlite".format( 54 | pb.get_outputs_dir() 55 | ) 56 | recorder = om.SqliteRecorder(self.case_recorder_filename) 57 | pb.model.add_recorder(recorder) 58 | self.pb.run_driver() 59 | self.assertTrue(os.path.exists(self.case_recorder_filename)) 60 | reader = om.CaseReader(self.case_recorder_filename) 61 | for case_id in reader.list_cases(): 62 | case = reader.get_case(case_id) 63 | print(f"obj = {case.outputs['obj']}") 64 | 65 | @unittest.skipIf(EGOBOX_NOT_INSTALLED, "egobox is not installed") 66 | def test_branin(self): 67 | self.pb = pb = om.Problem(BraninMDA()) 68 | pb.model.add_design_var("x1", lower=-5, upper=10) 69 | pb.model.add_design_var("x2", lower=0, upper=15) 70 | pb.model.add_objective("obj") 71 | pb.model.add_constraint("con", upper=0) 72 | case_recorder_filename = "test_egobox_driver_branin.sqlite" 73 | self._check_recorder_file(pb, cstr=True, filename=case_recorder_filename) 74 | 75 | @unittest.skipIf(EGOBOX_NOT_INSTALLED, "egobox is not installed") 76 | def test_ackley(self): 77 | self.pb = pb = om.Problem(AckleyMDA()) 78 | pb.model.add_design_var("x", lower=-32.768, upper=32.768) 79 | pb.model.add_objective("obj") 80 | case_recorder_filename = "test_egobox_driver_ackley.sqlite" 81 | self._check_recorder_file(pb, cstr=False, filename=case_recorder_filename) 82 | 83 | def _check_recorder_file(self, pb, cstr, filename): 84 | pb.driver = EgoboxEgorDriver() 85 | pb.driver.options["optimizer"] = "EGOR" 86 | pb.driver.opt_settings["maxiter"] = 10 87 | pb.setup() 88 | self.case_recorder_filename = "{}/{}".format(pb.get_outputs_dir(), filename) 89 | recorder = om.SqliteRecorder(self.case_recorder_filename) 90 | pb.model.add_recorder(recorder) 91 | self.pb.run_driver() 92 | self.assertTrue(os.path.exists(self.case_recorder_filename)) 93 | reader = om.CaseReader(self.case_recorder_filename) 94 | for case_id in reader.list_cases(): 95 | case = reader.get_case(case_id) 96 | print(case.outputs["obj"]) 97 | 98 | 99 | if __name__ == "__main__": 100 | unittest.main() 101 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/test_onera_sego_driver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from openmdao.api import ( 4 | Problem, 5 | SqliteRecorder, 6 | CaseReader, 7 | ) 8 | from openmdao.test_suite.components.sellar_feature import SellarMDA 9 | from openmdao_extensions.onera_sego_driver import OneraSegoDriver 10 | from openmdao_extensions.onera_sego_driver import ONERASEGO_NOT_INSTALLED 11 | 12 | from openmdao_extensions.tests.functions_test import BraninMDA, AckleyMDA 13 | 14 | 15 | class TestSegoMoe(unittest.TestCase): 16 | def setUp(self): 17 | pass 18 | 19 | def tearDown(self): 20 | pass # os.remove(self.case_recorder_filename) 21 | 22 | @unittest.skipIf(ONERASEGO_NOT_INSTALLED, "SEGOMOE is not installed") 23 | def test_sellar(self): 24 | self.pb = pb = Problem(SellarMDA()) 25 | pb.model.add_design_var("x", lower=0, upper=10) 26 | pb.model.add_design_var("z", lower=0, upper=10) 27 | pb.model.add_objective("obj") 28 | pb.model.add_constraint("con1", upper=0) 29 | pb.model.add_constraint("con2", upper=0) 30 | pb.driver = OneraSegoDriver(optimizer="SEGOMOE") 31 | pb.driver.opt_settings["maxiter"] = 10 32 | pb.setup() 33 | 34 | self.case_recorder_filename = "{}/test_segomoe_driver_sellar.sqlite".format( 35 | pb.get_outputs_dir() 36 | ) 37 | recorder = SqliteRecorder(self.case_recorder_filename) 38 | pb.model.add_recorder(recorder) 39 | self.pb.run_driver() 40 | 41 | self.assertTrue(os.path.exists(self.case_recorder_filename)) 42 | reader = CaseReader(self.case_recorder_filename) 43 | for case_id in reader.list_cases(): 44 | case = reader.get_case(case_id) 45 | print(case.outputs["obj"]) 46 | 47 | @unittest.skipIf(ONERASEGO_NOT_INSTALLED, "SEGOMOE is not installed") 48 | def test_branin(self): 49 | self.pb = pb = Problem(BraninMDA()) 50 | pb.model.add_design_var("x1", lower=-5, upper=10) 51 | pb.model.add_design_var("x2", lower=0, upper=15) 52 | pb.model.add_objective("obj") 53 | pb.model.add_constraint("con", upper=0) 54 | case_recorder_filename = "test_segomoe_driver_branin.sqlite" 55 | self._check_recorder_file(pb, cstr=True, filename=case_recorder_filename) 56 | 57 | @unittest.skipIf(ONERASEGO_NOT_INSTALLED, "SEGOMOE is not installed") 58 | def test_ackley(self): 59 | self.pb = pb = Problem(AckleyMDA()) 60 | pb.model.add_design_var("x", lower=-32.768, upper=32.768) 61 | pb.model.add_objective("obj") 62 | case_recorder_filename = "test_segomoe_driver_ackley.sqlite" 63 | self._check_recorder_file(pb, cstr=False, filename=case_recorder_filename) 64 | 65 | def _check_recorder_file(self, pb, cstr, filename): 66 | pb.driver = OneraSegoDriver() 67 | pb.driver.options["optimizer"] = "SEGOMOE" 68 | pb.driver.opt_settings["maxiter"] = 10 69 | # default model 70 | n_var = 2 71 | mod_obj = { 72 | "name": "KRG", 73 | "corr": "squar_exp", 74 | "regr": "constant", 75 | "theta0": [1.0] * n_var, 76 | "thetaL": [0.1] * n_var, 77 | "thetaU": [10.0] * n_var, 78 | "normalize": True, 79 | } 80 | model_type = {"obj": mod_obj} 81 | if cstr: 82 | model_type["con"] = mod_obj 83 | pb.driver.opt_settings["model_type"] = model_type 84 | pb.setup() 85 | 86 | self.case_recorder_file_name = "{}/{}".format(pb.get_outputs_dir(), filename) 87 | recorder = SqliteRecorder(self.case_recorder_filename) 88 | pb.model.add_recorder(recorder) 89 | 90 | self.pb.run_driver() 91 | self.assertTrue(os.path.exists(self.case_recorder_filename)) 92 | reader = CaseReader(self.case_recorder_filename) 93 | for case_id in reader.list_cases(): 94 | case = reader.get_case(case_id) 95 | print(case.outputs["obj"]) 96 | 97 | 98 | if __name__ == "__main__": 99 | unittest.main() 100 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/test_openturns_doe_driver.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from openmdao.api import SqliteRecorder 3 | from openmdao.test_suite.components.sellar import SellarProblem 4 | from openmdao_extensions.openturns_doe_driver import OpenturnsDOEDriver 5 | from openmdao_extensions.openturns_doe_driver import OPENTURNS_NOT_INSTALLED 6 | import openturns as ot 7 | 8 | 9 | class TestOpenturnsDoeDriver(unittest.TestCase): 10 | @staticmethod 11 | def run_driver(name, driver): 12 | pb = SellarProblem() 13 | pb.setup() 14 | 15 | case_recorder_filename = "{}/test_openturns_doe_{}.sqlite".format( 16 | pb.get_outputs_dir(), name 17 | ) 18 | recorder = SqliteRecorder(case_recorder_filename) 19 | pb.driver = driver 20 | pb.driver.add_recorder(recorder) 21 | 22 | pb.run_driver() 23 | pb.cleanup() 24 | 25 | @unittest.skipIf(OPENTURNS_NOT_INSTALLED, "OpenTURNS not installed") 26 | def test_openturns_doe_driver(self): 27 | ns = 100 28 | driver = OpenturnsDOEDriver(n_samples=ns) 29 | TestOpenturnsDoeDriver.run_driver("mc", driver) 30 | cases = driver.get_cases() 31 | self.assertEqual((100, 3), cases.shape) 32 | 33 | @unittest.skipIf(OPENTURNS_NOT_INSTALLED, "OpenTURNS not installed") 34 | def test_openturns_doe_driver_with_dist(self): 35 | ns = 100 36 | dists = [ot.Normal(2, 1), ot.Normal(5, 1), ot.Normal(2, 1)] 37 | driver = OpenturnsDOEDriver( 38 | n_samples=ns, distribution=ot.ComposedDistribution(dists) 39 | ) 40 | TestOpenturnsDoeDriver.run_driver("dist", driver) 41 | cases = driver.get_cases() 42 | self.assertEqual((100, 3), cases.shape) 43 | 44 | @unittest.skipIf(OPENTURNS_NOT_INSTALLED, "OpenTURNS not installed") 45 | def test_bad_dist(self): 46 | ns = 100 47 | dists = [ot.Normal(2, 1), ot.Normal(5, 1)] 48 | driver = OpenturnsDOEDriver( 49 | n_samples=ns, distribution=ot.ComposedDistribution(dists) 50 | ) 51 | 52 | with self.assertRaises(RuntimeError): 53 | TestOpenturnsDoeDriver.run_driver("bad", driver) 54 | 55 | 56 | if __name__ == "__main__": 57 | unittest.main() 58 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/test_reckless_nlbgs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | from openmdao.api import Problem, IndepVarComp 5 | from openmdao.test_suite.components.sellar import SellarDis1, SellarDis2 6 | from openmdao.utils.assert_utils import assert_near_equal 7 | from openmdao_extensions.reckless_nonlinear_block_gs import RecklessNonlinearBlockGS 8 | 9 | 10 | class ContrivedSellarDis1(SellarDis1): 11 | def setup(self): 12 | super(ContrivedSellarDis1, self).setup() 13 | self.add_output("highly_nonlinear", val=1.0) 14 | 15 | def compute(self, inputs, outputs): 16 | super(ContrivedSellarDis1, self).compute(inputs, outputs) 17 | outputs["highly_nonlinear"] = 10 * np.sin(10 * inputs["y2"]) 18 | 19 | 20 | class TestRecklessNLBGS(unittest.TestCase): 21 | def setUp(self): 22 | self.prob = Problem() 23 | model = self.prob.model 24 | 25 | model.add_subsystem("px", IndepVarComp("x", 1.0), promotes=["x"]) 26 | model.add_subsystem( 27 | "pz", IndepVarComp("z", np.array([5.0, 2.0])), promotes=["z"] 28 | ) 29 | 30 | model.add_subsystem( 31 | "d1", ContrivedSellarDis1(), promotes=["x", "z", "y1", "y2"] 32 | ) 33 | model.add_subsystem("d2", SellarDis2(), promotes=["z", "y1", "y2"]) 34 | 35 | self.nlbgs = nlbgs = model.nonlinear_solver = RecklessNonlinearBlockGS() 36 | 37 | nlbgs.options["maxiter"] = 20 38 | nlbgs.options["atol"] = 1e-6 39 | nlbgs.options["rtol"] = 1e-6 40 | nlbgs.options["iprint"] = 2 41 | 42 | def test_convergence_variables(self): 43 | prob = self.prob 44 | nlbgs = self.nlbgs 45 | 46 | prob.setup() 47 | prob.run_model() 48 | nb1 = nlbgs._iter_count 49 | 50 | assert_near_equal(prob["y1"], 25.58830273, 0.00001) 51 | assert_near_equal(prob["y2"], 12.05848819, 0.00001) 52 | 53 | nlbgs.options["convrg_vars"] = ["d1.y1", "d2.y2"] 54 | 55 | prob.setup() 56 | prob.run_model() 57 | nb2 = nlbgs._iter_count 58 | self.assertLess(nb2, nb1) 59 | 60 | nlbgs.options["convrg_rtols"] = [1e-3, 1e-3] 61 | 62 | prob.setup() 63 | prob.run_model() 64 | nb3 = nlbgs._iter_count 65 | self.assertLessEqual(nb3, nb2) 66 | 67 | assert_near_equal(prob["y1"], 25.58830273, 0.00001) 68 | assert_near_equal(prob["y2"], 12.05848819, 0.00001) 69 | 70 | def test_bad_size(self): 71 | self.nlbgs.options["convrg_vars"] = ["d1.y1", "d2.y2"] 72 | self.nlbgs.options["convrg_rtols"] = [1e-3] 73 | 74 | self.prob.setup() 75 | with self.assertRaises(RuntimeError) as context: 76 | self.prob.run_model() 77 | self.assertEqual( 78 | str(context.exception), "Convergence rtols bad size : should be 2, found 1." 79 | ) 80 | 81 | 82 | if __name__ == "__main__": 83 | unittest.main() 84 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/test_salib_doe_driver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from openmdao.api import SqliteRecorder, CaseReader 4 | from openmdao.test_suite.components.sellar import SellarProblem 5 | from openmdao_extensions.salib_doe_driver import SalibDOEDriver 6 | from openmdao_extensions.salib_doe_driver import SALIB_NOT_INSTALLED 7 | 8 | 9 | class TestSalibDoeDriver(unittest.TestCase): 10 | @staticmethod 11 | def run_driver(name, driver): 12 | pb = SellarProblem() 13 | pb.driver = driver 14 | pb.setup() 15 | 16 | case_recorder_filename = "{}/test_salib_doe_{}.sqlite".format( 17 | pb.get_outputs_dir(), name 18 | ) 19 | recorder = SqliteRecorder(case_recorder_filename) 20 | pb.driver.add_recorder(recorder) 21 | pb.run_driver() 22 | pb.cleanup() 23 | return pb, case_recorder_filename 24 | 25 | def assert_morris_case_generation(self, nt, driver): 26 | pb, case_recorder_filename = TestSalibDoeDriver.run_driver( 27 | "morris" + str(nt), driver 28 | ) 29 | 30 | self.assertTrue(os.path.exists(case_recorder_filename)) 31 | reader = CaseReader(case_recorder_filename) 32 | cases = reader.list_cases("driver") 33 | os.remove(case_recorder_filename) 34 | n = sum(data["size"] for data in pb.model.get_design_vars().values()) 35 | self.assertEqual(len(cases), (n + 1) * nt) 36 | 37 | @unittest.skipIf(SALIB_NOT_INSTALLED, "SALib library is not installed") 38 | def test_salib_morris_driver(self): 39 | nt = 4 40 | driver = SalibDOEDriver(sa_method_name="Morris", sa_doe_options={"n_trajs": nt}) 41 | self.assert_morris_case_generation(nt, driver) 42 | salib_cases = driver.get_cases() 43 | self.assertTrue(len(salib_cases) > 0) 44 | salib_pb = driver.get_salib_problem() 45 | self.assertTrue(salib_pb) 46 | 47 | @unittest.skipIf(SALIB_NOT_INSTALLED, "SALib library is not installed") 48 | def test_salib_sobol_driver(self): 49 | ns = 100 50 | driver = SalibDOEDriver( 51 | sa_method_name="Sobol", 52 | sa_doe_options={"n_samples": ns, "calc_second_order": True}, 53 | ) 54 | TestSalibDoeDriver.run_driver("sobol", driver) 55 | salib_cases = driver.get_cases() 56 | self.assertTrue(len(salib_cases) > 0) 57 | salib_pb = driver.get_salib_problem() 58 | self.assertTrue(salib_pb) 59 | 60 | # @unittest.skipIf(SALIB_NOT_INSTALLED, 'SALib library is not installed') 61 | # def test_doe_generator(self): 62 | # nt = 5 63 | # self.assert_morris_case_generation(nt, DOEDriver(SalibMorrisDOEGenerator(n_trajs=nt, n_levels=4))) 64 | 65 | 66 | if __name__ == "__main__": 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /openmdao_extensions/tests/test_smt_doe_driver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from openmdao.api import SqliteRecorder, CaseReader 4 | from openmdao.test_suite.components.sellar import SellarProblem 5 | from openmdao_extensions.smt_doe_driver import SmtDOEDriver 6 | from openmdao_extensions.smt_doe_driver import SMT_NOT_INSTALLED 7 | 8 | 9 | class TestSmtDoeDriver(unittest.TestCase): 10 | def assert_case_generation(self, n, driver): 11 | pb = SellarProblem() 12 | pb.driver = driver 13 | pb.setup() 14 | 15 | case_recorder_filename = "{}/test_smt_doe_driver_{}.sqlite".format( 16 | pb.get_outputs_dir(), n 17 | ) 18 | recorder = SqliteRecorder(case_recorder_filename) 19 | pb.driver.add_recorder(recorder) 20 | pb.run_driver() 21 | pb.cleanup() 22 | 23 | reader = CaseReader(case_recorder_filename) 24 | cases = reader.list_cases("driver") 25 | os.remove(case_recorder_filename) 26 | self.assertEqual(len(cases), n) 27 | # for i in range(len(cases)): 28 | # case = reader.get_case(cases[i]) 29 | # print(case) 30 | 31 | @unittest.skipIf(SMT_NOT_INSTALLED, "SMT library is not installed") 32 | def test_smt_lhs_doe_driver(self): 33 | n = 10 34 | self.assert_case_generation( 35 | n, 36 | SmtDOEDriver( 37 | sampling_method_name="LHS", 38 | n_cases=n, 39 | sampling_method_options={"criterion": "ese"}, 40 | ), 41 | ) 42 | 43 | @unittest.skipIf(SMT_NOT_INSTALLED, "SMT library is not installed") 44 | def test_smt_ff_doe_driver(self): 45 | n = 11 46 | self.assert_case_generation( 47 | n, SmtDOEDriver(sampling_method_name="FullFactorial", n_cases=n) 48 | ) 49 | 50 | @unittest.skipIf(SMT_NOT_INSTALLED, "SMT library is not installed") 51 | def test_smt_rand_doe_driver(self): 52 | n = 12 53 | self.assert_case_generation( 54 | n, SmtDOEDriver(sampling_method_name="Random", n_cases=n) 55 | ) 56 | 57 | 58 | if __name__ == "__main__": 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | smt 2 | salib 3 | openturns 4 | egobox 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Remi Lafage 3 | 4 | This package is distributed under Apache 2 license. 5 | """ 6 | 7 | from setuptools import setup 8 | 9 | from os import path 10 | from io import open 11 | from openmdao_extensions import __version__ 12 | 13 | this_directory = path.abspath(path.dirname(__file__)) 14 | with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: 15 | long_description = f.read() 16 | 17 | CLASSIFIERS = """ 18 | Development Status :: 5 - Production/Stable 19 | Intended Audience :: Science/Research 20 | Intended Audience :: Developers 21 | License :: OSI Approved :: Apache Software License 22 | Programming Language :: Python 23 | Programming Language :: Python :: 3 24 | Topic :: Software Development 25 | Topic :: Scientific/Engineering 26 | Operating System :: Microsoft :: Windows 27 | Operating System :: Unix 28 | Operating System :: MacOS 29 | """ 30 | 31 | metadata = dict( 32 | name="openmdao_extensions", 33 | version=__version__, 34 | description="Additional solvers and drivers for OpenMDAO framework", 35 | long_description=long_description, 36 | long_description_content_type="text/markdown", 37 | author="Rémi Lafage", 38 | author_email="remi.lafage@onera.fr", 39 | license="Apache License, Version 2.0", 40 | classifiers=[_f for _f in CLASSIFIERS.split("\n") if _f], 41 | packages=["openmdao_extensions"], 42 | install_requires=["openmdao>=3.0.0"], 43 | extras_require={ 44 | "egobox": [ 45 | "egobox>=0.26.0", 46 | ] 47 | }, 48 | python_requires=">=3.8", 49 | zip_safe=True, 50 | url="https://github.com/OneraHub/openmdao_extensions", 51 | download_url="https://github.com/OneraHub/openmdao_extensions/releases", 52 | keywords="openmdao openmdao_driver openmdao_nl_solver", 53 | entry_points={ 54 | "openmdao_driver": [ 55 | "egoboxegordriver = openmdao_extensions.egobox_egor_driver:EgoboxEgorDriver", 56 | "onerasegodriver = openmdao_extensions.onera_sego_driver:OneraSegoDriver", 57 | "openturnsdoedriver = openmdao_extensions.openturns_doe_driver:OpenturnsDOEDriver", 58 | "salibdoedriver = openmdao_extensions.salib_doe_driver:SalibDOEDriver", 59 | "smtdoedriver = openmdao_extensions.smt_doe_driver:SmtDOEDriver", 60 | ], 61 | "openmdao_nl_solver": [ 62 | "recklessnonlinearblockgs = openmdao_extensions.reckless_nonlinear_block_gs:RecklessNonlinearBlockGS" 63 | ], 64 | }, 65 | ) 66 | 67 | setup(**metadata) 68 | --------------------------------------------------------------------------------