ropt-dakota 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ropt_dakota-0.1.0/.github/workflows/static-checks.yml +42 -0
- ropt_dakota-0.1.0/.github/workflows/tests.yml +33 -0
- ropt_dakota-0.1.0/.gitignore +3 -0
- ropt_dakota-0.1.0/PKG-INFO +39 -0
- ropt_dakota-0.1.0/README-PyPI.md +16 -0
- ropt_dakota-0.1.0/README.md +26 -0
- ropt_dakota-0.1.0/pyproject.toml +90 -0
- ropt_dakota-0.1.0/setup.cfg +4 -0
- ropt_dakota-0.1.0/src/ropt_dakota/__init__.py +1 -0
- ropt_dakota-0.1.0/src/ropt_dakota/dakota.py +468 -0
- ropt_dakota-0.1.0/src/ropt_dakota/py.typed +0 -0
- ropt_dakota-0.1.0/src/ropt_dakota/version.py +16 -0
- ropt_dakota-0.1.0/src/ropt_dakota.egg-info/PKG-INFO +39 -0
- ropt_dakota-0.1.0/src/ropt_dakota.egg-info/SOURCES.txt +19 -0
- ropt_dakota-0.1.0/src/ropt_dakota.egg-info/dependency_links.txt +1 -0
- ropt_dakota-0.1.0/src/ropt_dakota.egg-info/entry_points.txt +2 -0
- ropt_dakota-0.1.0/src/ropt_dakota.egg-info/requires.txt +7 -0
- ropt_dakota-0.1.0/src/ropt_dakota.egg-info/top_level.txt +1 -0
- ropt_dakota-0.1.0/tests/__init__.py +0 -0
- ropt_dakota-0.1.0/tests/conftest.py +72 -0
- ropt_dakota-0.1.0/tests/test_dakota_backend.py +369 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Run static checks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
schedule:
|
|
10
|
+
- cron: '43 1 * * 1'
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
static-checks:
|
|
14
|
+
runs-on: ubuntu-22.04
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.10"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
python -m pip install ropt
|
|
29
|
+
python -m pip install .[test]
|
|
30
|
+
- name: Run ruff format
|
|
31
|
+
if: always()
|
|
32
|
+
run: |
|
|
33
|
+
python -m ruff format --check src/ropt_dakota tests
|
|
34
|
+
- name: Run ruff
|
|
35
|
+
if: always()
|
|
36
|
+
run: |
|
|
37
|
+
python -m ruff check src/ropt_dakota tests
|
|
38
|
+
- name: Run mypy
|
|
39
|
+
if: always()
|
|
40
|
+
run: |
|
|
41
|
+
python -m mypy src/ropt_dakota tests
|
|
42
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Run tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
schedule:
|
|
10
|
+
- cron: '43 1 * * 1'
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
run-tests:
|
|
14
|
+
runs-on: ubuntu-22.04
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
python -m pip install ropt
|
|
29
|
+
python -m pip install pytest
|
|
30
|
+
python -m pip install .
|
|
31
|
+
- name: Run pytest
|
|
32
|
+
run: |
|
|
33
|
+
python -m pytest tests
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ropt-dakota
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Dakota optimizer plugin for ropt
|
|
5
|
+
Classifier: Development Status :: 4 - Beta
|
|
6
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
7
|
+
Classifier: Natural Language :: English
|
|
8
|
+
Classifier: Programming Language :: Python
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: carolina
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: ruff; extra == "test"
|
|
21
|
+
Requires-Dist: mypy; extra == "test"
|
|
22
|
+
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
|
|
24
|
+
# A Dakota optimizer plugin for `ropt`
|
|
25
|
+
This package installs a plugin for the `ropt` robust optimizer package, giving
|
|
26
|
+
access to algorithms from the Dakota optimization package.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
This code has been tested with Python versions 3.8, 3.9, 3.10 and 3.11.
|
|
31
|
+
|
|
32
|
+
The backend is based on the [Dakota](https://dakota.sandia.gov/) optimizer and
|
|
33
|
+
depends on the [Carolina](https://github.com/equinor/Carolina) Python wrapper.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
```bash
|
|
38
|
+
pip install ropt-dakota
|
|
39
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# A Dakota optimizer plugin for `ropt`
|
|
2
|
+
This package installs a plugin for the `ropt` robust optimizer package, giving
|
|
3
|
+
access to algorithms from the Dakota optimization package.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Dependencies
|
|
7
|
+
This code has been tested with Python versions 3.8, 3.9, 3.10 and 3.11.
|
|
8
|
+
|
|
9
|
+
The backend is based on the [Dakota](https://dakota.sandia.gov/) optimizer and
|
|
10
|
+
depends on the [Carolina](https://github.com/equinor/Carolina) Python wrapper.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
```bash
|
|
15
|
+
pip install ropt-dakota
|
|
16
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# A Dakota optimizer plugin for `ropt`
|
|
2
|
+
This package installs a plugin for the `ropt` robust optimizer package, giving
|
|
3
|
+
access to algorithms from the Dakota optimization package.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Dependencies
|
|
7
|
+
This code has been tested with Python versions 3.8, 3.9, 3.10 and 3.11.
|
|
8
|
+
|
|
9
|
+
The backend is based on the [Dakota](https://dakota.sandia.gov/) optimizer and
|
|
10
|
+
depends on the [Carolina](https://github.com/equinor/Carolina) Python wrapper.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
To install, enter the distribution directory and execute:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Running the tests
|
|
21
|
+
To run the test suite, install the necessary dependencies and execute `pytest`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install .[test]
|
|
25
|
+
pytest
|
|
26
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "setuptools-scm"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ropt-dakota"
|
|
7
|
+
description = "A Dakota optimizer plugin for ropt"
|
|
8
|
+
readme = "README-PyPI.md"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
12
|
+
"Natural Language :: English",
|
|
13
|
+
"Programming Language :: Python",
|
|
14
|
+
"Programming Language :: Python :: 3.8",
|
|
15
|
+
"Programming Language :: Python :: 3.9",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
]
|
|
21
|
+
requires-python = ">=3.8"
|
|
22
|
+
dynamic = ["version"]
|
|
23
|
+
dependencies = ["numpy", "carolina"]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = ["ruff", "mypy", "pytest"]
|
|
27
|
+
|
|
28
|
+
[project.entry-points."ropt.plugins.optimizer"]
|
|
29
|
+
dakota = "ropt_dakota.dakota:DakotaOptimizer"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
include = ["ropt_dakota"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.package-data]
|
|
36
|
+
ropt_dakota = ["py.typed"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.dynamic]
|
|
39
|
+
version = { attr = "ropt_dakota.version.__version__" }
|
|
40
|
+
|
|
41
|
+
[tool.setuptools_scm]
|
|
42
|
+
write_to = "src/ropt_dakota/version.py"
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
filterwarnings = [
|
|
46
|
+
"error",
|
|
47
|
+
'ignore:.*Pydantic will allow any object with no validation.*:UserWarning', # 3.8
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
select = ["ALL"]
|
|
52
|
+
ignore = [
|
|
53
|
+
"ANN101",
|
|
54
|
+
"ANN102",
|
|
55
|
+
"AIR",
|
|
56
|
+
"COM812",
|
|
57
|
+
"COM819",
|
|
58
|
+
"D206",
|
|
59
|
+
"E501",
|
|
60
|
+
"DJ",
|
|
61
|
+
"FA",
|
|
62
|
+
"ISC001",
|
|
63
|
+
"PGH",
|
|
64
|
+
"UP",
|
|
65
|
+
"ANN101",
|
|
66
|
+
"ANN102",
|
|
67
|
+
"FIX002",
|
|
68
|
+
"S101",
|
|
69
|
+
"TD002",
|
|
70
|
+
"TD003",
|
|
71
|
+
"Q",
|
|
72
|
+
"UP",
|
|
73
|
+
"W191",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.format]
|
|
77
|
+
exclude = ["src/ropt_dakota/version.py", "build"]
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint.pydocstyle]
|
|
80
|
+
convention = "google"
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint.per-file-ignores]
|
|
83
|
+
"tests/*" = ["D", "ANN401", "PLR2004"]
|
|
84
|
+
|
|
85
|
+
[tool.mypy]
|
|
86
|
+
strict = true
|
|
87
|
+
|
|
88
|
+
[[tool.mypy.overrides]]
|
|
89
|
+
module = ["dakota", "ropt_dakota.version"]
|
|
90
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""A Dakota optimizer plugin for ropt."""
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""This module implements the Dakota optimization plugin."""
|
|
2
|
+
|
|
3
|
+
from math import isfinite
|
|
4
|
+
from os import chdir
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from tempfile import TemporaryDirectory
|
|
7
|
+
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from dakota import _USER_DATA, DakotaBase, DakotaInput, run_dakota
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
from ropt.config.enopt import EnOptConfig, OptimizerConfig
|
|
13
|
+
from ropt.enums import ConstraintType
|
|
14
|
+
from ropt.exceptions import ConfigError
|
|
15
|
+
from ropt.plugins.optimizer.protocol import OptimizerCallback
|
|
16
|
+
from ropt.plugins.optimizer.utils import create_output_path, filter_linear_constraints
|
|
17
|
+
|
|
18
|
+
_PRECISION: int = 8
|
|
19
|
+
_LARGE_NUMBER_FOR_INF = 1e30
|
|
20
|
+
|
|
21
|
+
_ConstraintIndices = Tuple[
|
|
22
|
+
NDArray[np.intc],
|
|
23
|
+
NDArray[np.intc],
|
|
24
|
+
NDArray[np.intc],
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DakotaOptimizer:
|
|
29
|
+
"""Backend class for optimization via Dakota."""
|
|
30
|
+
|
|
31
|
+
SUPPORTED_ALGORITHMS: ClassVar[Set[str]] = {
|
|
32
|
+
"optpp_q_newton",
|
|
33
|
+
"conmin_mfd",
|
|
34
|
+
"conmin_frcg",
|
|
35
|
+
"mesh_adaptive_search",
|
|
36
|
+
"coliny_ea",
|
|
37
|
+
"soga",
|
|
38
|
+
"moga",
|
|
39
|
+
"asynch_pattern_search",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self, config: EnOptConfig, optimizer_callback: OptimizerCallback
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Initialize the optimizer implemented by the Dakota plugin.
|
|
46
|
+
|
|
47
|
+
See the [ropt.plugins.optimizer.protocol.Optimizer][] protocol.
|
|
48
|
+
|
|
49
|
+
# noqa
|
|
50
|
+
"""
|
|
51
|
+
self._config = config
|
|
52
|
+
self._optimizer_callback = optimizer_callback
|
|
53
|
+
self._constraint_indices = self._get_constraint_indices()
|
|
54
|
+
self._output_dir: Path
|
|
55
|
+
|
|
56
|
+
def start(self, initial_values: NDArray[np.float64]) -> None:
|
|
57
|
+
"""Start the optimization.
|
|
58
|
+
|
|
59
|
+
See the [ropt.plugins.optimizer.protocol.Optimizer][] protocol.
|
|
60
|
+
|
|
61
|
+
# noqa
|
|
62
|
+
"""
|
|
63
|
+
if self._config.optimizer.output_dir is None:
|
|
64
|
+
with TemporaryDirectory() as output_dir:
|
|
65
|
+
self._output_dir = Path(output_dir)
|
|
66
|
+
self._start(initial_values)
|
|
67
|
+
else:
|
|
68
|
+
self._output_dir = self._config.optimizer.output_dir
|
|
69
|
+
self._start(initial_values)
|
|
70
|
+
|
|
71
|
+
def _get_constraint_indices(self) -> Optional[_ConstraintIndices]:
|
|
72
|
+
if self._config.nonlinear_constraints is None:
|
|
73
|
+
return None
|
|
74
|
+
types = self._config.nonlinear_constraints.types
|
|
75
|
+
return (
|
|
76
|
+
np.fromiter(
|
|
77
|
+
(idx for idx, type_ in enumerate(types) if type_ == ConstraintType.LE),
|
|
78
|
+
dtype=np.intc,
|
|
79
|
+
),
|
|
80
|
+
np.fromiter(
|
|
81
|
+
(idx for idx, type_ in enumerate(types) if type_ == ConstraintType.GE),
|
|
82
|
+
dtype=np.intc,
|
|
83
|
+
),
|
|
84
|
+
np.fromiter(
|
|
85
|
+
(idx for idx, type_ in enumerate(types) if type_ == ConstraintType.EQ),
|
|
86
|
+
dtype=np.intc,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _get_inputs(self, initial_values: NDArray[np.float64]) -> Dict[str, List[str]]:
|
|
91
|
+
return {
|
|
92
|
+
"environment": [
|
|
93
|
+
"tabular_graphics_data",
|
|
94
|
+
"output_precision = 8",
|
|
95
|
+
],
|
|
96
|
+
"method": self._get_method_section(),
|
|
97
|
+
"model": ["single"],
|
|
98
|
+
"variables": (
|
|
99
|
+
self._get_variables_section(initial_values)
|
|
100
|
+
+ self._get_linear_constraints_section()
|
|
101
|
+
),
|
|
102
|
+
"responses": [
|
|
103
|
+
"num_objective_functions = 1",
|
|
104
|
+
"analytic_gradients",
|
|
105
|
+
"no_hessians",
|
|
106
|
+
*self._get_responses_section(),
|
|
107
|
+
],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def _get_method_section(self) -> List[str]:
|
|
111
|
+
inputs: List[str] = []
|
|
112
|
+
algorithm = self._config.optimizer.algorithm
|
|
113
|
+
if algorithm is None:
|
|
114
|
+
algorithm = "optpp_q_newton"
|
|
115
|
+
inputs.append(algorithm)
|
|
116
|
+
# Scaling is always on
|
|
117
|
+
inputs.append("scaling")
|
|
118
|
+
iterations = self._config.optimizer.max_iterations
|
|
119
|
+
if iterations is not None and algorithm != "asynch_pattern_search":
|
|
120
|
+
inputs.append(f"max_iterations = {iterations}")
|
|
121
|
+
convergence_tolerance = self._config.optimizer.tolerance
|
|
122
|
+
if convergence_tolerance is not None:
|
|
123
|
+
tolerance_option = (
|
|
124
|
+
"variable_tolerance"
|
|
125
|
+
if algorithm in ["mesh_adaptive_search", "asynch_pattern_search"]
|
|
126
|
+
else "convergence_tolerance"
|
|
127
|
+
)
|
|
128
|
+
inputs.append(f"{tolerance_option} = {convergence_tolerance}")
|
|
129
|
+
# The Dakota interface seems not be able to deal with with speculative
|
|
130
|
+
# and split_evaluations simultaneously. Do not enable speculative if
|
|
131
|
+
# split_evaluations is defined.
|
|
132
|
+
if (
|
|
133
|
+
self._config.optimizer.speculative
|
|
134
|
+
and not self._config.optimizer.split_evaluations
|
|
135
|
+
):
|
|
136
|
+
inputs.append("speculative")
|
|
137
|
+
# Options are put in the method section:
|
|
138
|
+
return inputs + self._get_options(algorithm)
|
|
139
|
+
|
|
140
|
+
def _get_options(self, algorithm: str) -> List[str]:
|
|
141
|
+
inputs: List[str] = []
|
|
142
|
+
if isinstance(self._config.optimizer.options, list):
|
|
143
|
+
try:
|
|
144
|
+
for option in self._config.optimizer.options:
|
|
145
|
+
stripped = option.strip()
|
|
146
|
+
if stripped.startswith("input_file"):
|
|
147
|
+
continue
|
|
148
|
+
if algorithm in [
|
|
149
|
+
"conmin_mfd",
|
|
150
|
+
"conmin_frcg",
|
|
151
|
+
] and stripped.startswith("constraint_tolerance"):
|
|
152
|
+
continue
|
|
153
|
+
inputs.append(f"{option}")
|
|
154
|
+
except TypeError as exc:
|
|
155
|
+
msg = "Cannot parse Dakota optimization options"
|
|
156
|
+
raise ConfigError(msg) from exc
|
|
157
|
+
return inputs
|
|
158
|
+
|
|
159
|
+
def _get_variables_section(self, initial_values: NDArray[np.float64]) -> List[str]:
|
|
160
|
+
inputs: List[str] = []
|
|
161
|
+
names = self._config.variables.names
|
|
162
|
+
if names is None:
|
|
163
|
+
names = tuple(f"variable{idx}" for idx in range(initial_values.size))
|
|
164
|
+
lower_bounds = self._config.variables.lower_bounds
|
|
165
|
+
upper_bounds = self._config.variables.upper_bounds
|
|
166
|
+
variable_indices = self._config.variables.indices
|
|
167
|
+
if variable_indices is not None:
|
|
168
|
+
initial_values = initial_values[variable_indices]
|
|
169
|
+
names = tuple(names[idx] for idx in variable_indices)
|
|
170
|
+
lower_bounds = lower_bounds[variable_indices]
|
|
171
|
+
upper_bounds = upper_bounds[variable_indices]
|
|
172
|
+
inputs.append(f"continuous_design = {initial_values.size}")
|
|
173
|
+
inputs.append(
|
|
174
|
+
"initial_point "
|
|
175
|
+
+ " ".join(
|
|
176
|
+
f"{initial_value:{_PRECISION}f}" for initial_value in initial_values
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
inputs.append(
|
|
180
|
+
"lower_bounds "
|
|
181
|
+
+ " ".join(
|
|
182
|
+
f"{bound:{_PRECISION}f}"
|
|
183
|
+
if isfinite(bound)
|
|
184
|
+
else f"-{_LARGE_NUMBER_FOR_INF}"
|
|
185
|
+
for bound in lower_bounds
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
inputs.append(
|
|
189
|
+
"upper_bounds "
|
|
190
|
+
+ " ".join(
|
|
191
|
+
f"{bound:{_PRECISION}f}"
|
|
192
|
+
if isfinite(bound)
|
|
193
|
+
else f"+{_LARGE_NUMBER_FOR_INF}"
|
|
194
|
+
for bound in upper_bounds
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
variable_names = [
|
|
198
|
+
"_".join(str(name) for name in variable_name)
|
|
199
|
+
if isinstance(variable_name, tuple)
|
|
200
|
+
else str(variable_name)
|
|
201
|
+
for variable_name in names
|
|
202
|
+
]
|
|
203
|
+
inputs.append(
|
|
204
|
+
"descriptors " + " ".join(f"'{name}'" for name in variable_names),
|
|
205
|
+
)
|
|
206
|
+
return inputs
|
|
207
|
+
|
|
208
|
+
def _get_linear_constraints(
|
|
209
|
+
self,
|
|
210
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.ubyte]]:
|
|
211
|
+
linear_constraints_config = self._config.linear_constraints
|
|
212
|
+
assert linear_constraints_config is not None
|
|
213
|
+
|
|
214
|
+
if self._config.variables.indices is not None:
|
|
215
|
+
linear_constraints_config = filter_linear_constraints(
|
|
216
|
+
linear_constraints_config, self._config.variables.indices
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
coefficients = linear_constraints_config.coefficients
|
|
220
|
+
rhs_values = linear_constraints_config.rhs_values
|
|
221
|
+
types = linear_constraints_config.types
|
|
222
|
+
|
|
223
|
+
return coefficients, rhs_values, types
|
|
224
|
+
|
|
225
|
+
def _get_linear_constraints_section(self) -> List[str]:
|
|
226
|
+
inputs: List[str] = []
|
|
227
|
+
|
|
228
|
+
if self._config.linear_constraints is not None:
|
|
229
|
+
all_coefficients, all_rhs_values, all_types = self._get_linear_constraints()
|
|
230
|
+
|
|
231
|
+
coefficients = all_coefficients[all_types != ConstraintType.EQ, :]
|
|
232
|
+
rhs_values = all_rhs_values[all_types != ConstraintType.EQ]
|
|
233
|
+
rhs_values[rhs_values < -_LARGE_NUMBER_FOR_INF] = -_LARGE_NUMBER_FOR_INF
|
|
234
|
+
rhs_values[rhs_values > _LARGE_NUMBER_FOR_INF] = _LARGE_NUMBER_FOR_INF
|
|
235
|
+
types = all_types[all_types != ConstraintType.EQ]
|
|
236
|
+
if types.size > 0:
|
|
237
|
+
# Inequality linear constraints are defined as one-sided with a lower
|
|
238
|
+
# bound. Earlier versions of this driver used upper bounds, and we stick
|
|
239
|
+
# to that by using the negatives of the matrix and bound values for ge
|
|
240
|
+
# constraints.
|
|
241
|
+
coefficients[types == ConstraintType.GE, :] = -coefficients[
|
|
242
|
+
types == ConstraintType.GE, :
|
|
243
|
+
]
|
|
244
|
+
rhs_values[types == ConstraintType.GE] = -rhs_values[
|
|
245
|
+
types == ConstraintType.GE
|
|
246
|
+
]
|
|
247
|
+
# Add 0.0 to prevent -0.0 values:
|
|
248
|
+
coefficients += 0.0
|
|
249
|
+
rhs_values += 0.0
|
|
250
|
+
inputs.append(
|
|
251
|
+
"linear_inequality_constraint_matrix = "
|
|
252
|
+
+ "\n".join(
|
|
253
|
+
" ".join(
|
|
254
|
+
f"{value:{_PRECISION}f}" for value in coefficients[idx, :]
|
|
255
|
+
)
|
|
256
|
+
for idx in range(types.size)
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
inputs.append(
|
|
260
|
+
"linear_inequality_lower_bounds = "
|
|
261
|
+
+ " ".join(["-1e+30"] * types.size),
|
|
262
|
+
)
|
|
263
|
+
inputs.append(
|
|
264
|
+
"linear_inequality_upper_bounds = "
|
|
265
|
+
+ " ".join(f"{value:{_PRECISION}f}" for value in rhs_values),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
coefficients = all_coefficients[all_types == ConstraintType.EQ, :]
|
|
269
|
+
rhs_values = all_rhs_values[all_types == ConstraintType.EQ]
|
|
270
|
+
types = all_types[all_types == ConstraintType.EQ]
|
|
271
|
+
if types.size > 0:
|
|
272
|
+
inputs.append(
|
|
273
|
+
"linear_equality_constraint_matrix = "
|
|
274
|
+
+ "\n".join(
|
|
275
|
+
" ".join(
|
|
276
|
+
f"{value:{_PRECISION}f}" for value in coefficients[idx, :]
|
|
277
|
+
)
|
|
278
|
+
for idx in range(types.size)
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
inputs.append(
|
|
282
|
+
"linear_equality_targets ="
|
|
283
|
+
+ " ".join(f"{value:{_PRECISION}f}" for value in rhs_values),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return inputs
|
|
287
|
+
|
|
288
|
+
def _get_responses_section(self) -> List[str]:
|
|
289
|
+
inputs: List[str] = []
|
|
290
|
+
# ropt currently only allows for a single objective function
|
|
291
|
+
inputs.append("objective_function_scale_types 'value'")
|
|
292
|
+
inputs.append("objective_function_scales = 1.0")
|
|
293
|
+
if self._constraint_indices is not None:
|
|
294
|
+
ge_indices, le_indices, eq_indices = self._constraint_indices
|
|
295
|
+
ineq_count = ge_indices.size + le_indices.size
|
|
296
|
+
if ineq_count > 0:
|
|
297
|
+
inputs.append(f"nonlinear_inequality_constraints = {ineq_count} ")
|
|
298
|
+
inputs.append("nonlinear_inequality_upper_bounds" + " 0.0" * ineq_count)
|
|
299
|
+
inputs.append("nonlinear_inequality_scale_types 'value'")
|
|
300
|
+
inputs.append("nonlinear_inequality_scales" + " 1.0" * ineq_count)
|
|
301
|
+
|
|
302
|
+
eq_count = eq_indices.size
|
|
303
|
+
if eq_count > 0:
|
|
304
|
+
inputs.append(f"nonlinear_equality_constraints = {eq_count}")
|
|
305
|
+
inputs.append("nonlinear_equality_targets" + " 0.0" * eq_count)
|
|
306
|
+
inputs.append("nonlinear_equality_scale_types 'value'")
|
|
307
|
+
inputs.append("nonlinear_equality_scales" + " 1.0" * eq_count)
|
|
308
|
+
return inputs
|
|
309
|
+
|
|
310
|
+
def _start(self, initial_values: NDArray[np.float64]) -> None:
|
|
311
|
+
pwd = Path.cwd()
|
|
312
|
+
output_dir = create_output_path("dakota", self._output_dir)
|
|
313
|
+
output_dir.mkdir()
|
|
314
|
+
chdir(output_dir)
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
self._start_direct_interface(initial_values)
|
|
318
|
+
finally:
|
|
319
|
+
if pwd.exists():
|
|
320
|
+
chdir(pwd)
|
|
321
|
+
|
|
322
|
+
def _start_direct_interface(self, initial_values: NDArray[np.float64]) -> None:
|
|
323
|
+
driver = _DakotaDriver(
|
|
324
|
+
self._config.optimizer,
|
|
325
|
+
self._optimizer_callback,
|
|
326
|
+
self._constraint_indices,
|
|
327
|
+
self._get_inputs(initial_values),
|
|
328
|
+
)
|
|
329
|
+
try:
|
|
330
|
+
driver.run_dakota(
|
|
331
|
+
infile="dakota_Input.in",
|
|
332
|
+
stdout="Report.txt",
|
|
333
|
+
stderr="dakota_errors.txt",
|
|
334
|
+
)
|
|
335
|
+
except Exception as err:
|
|
336
|
+
if driver.exception:
|
|
337
|
+
raise driver.exception from err
|
|
338
|
+
raise
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# mypy: disallow-subclassing-any=False
|
|
342
|
+
class _DakotaDriver(DakotaBase):
|
|
343
|
+
def __init__(
|
|
344
|
+
self,
|
|
345
|
+
optimizer_config: OptimizerConfig,
|
|
346
|
+
optimizer_callback: OptimizerCallback,
|
|
347
|
+
constraint_indices: Optional[_ConstraintIndices],
|
|
348
|
+
inputs: Dict[str, List[str]],
|
|
349
|
+
) -> None:
|
|
350
|
+
self._optimizer_config = optimizer_config
|
|
351
|
+
self._optimizer_callback = optimizer_callback
|
|
352
|
+
self._split_evaluations = optimizer_config.split_evaluations
|
|
353
|
+
self._constraint_indices = constraint_indices
|
|
354
|
+
self.exception: Optional[Exception] = None
|
|
355
|
+
super().__init__(DakotaInput(**inputs))
|
|
356
|
+
|
|
357
|
+
def dakota_callback(
|
|
358
|
+
self,
|
|
359
|
+
**kwargs: Any, # noqa: ANN401
|
|
360
|
+
) -> Dict[str, NDArray[np.float64]]:
|
|
361
|
+
try:
|
|
362
|
+
asv = [response for response in kwargs["asv"] if response > 0]
|
|
363
|
+
if len(set(asv)) > 1:
|
|
364
|
+
msg = "Non-unique ASV elements different from 0 are not yet supported."
|
|
365
|
+
raise NotImplementedError(msg) # noqa: TRY301
|
|
366
|
+
return_functions = asv[0] in (1, 3)
|
|
367
|
+
compute_gradients = asv[0] in (2, 3)
|
|
368
|
+
function_result, gradient_result = _compute_response(
|
|
369
|
+
np.concatenate(
|
|
370
|
+
(kwargs["cv"], kwargs["div"].astype(np.float64), kwargs["drv"]),
|
|
371
|
+
),
|
|
372
|
+
self._constraint_indices,
|
|
373
|
+
self._optimizer_callback,
|
|
374
|
+
return_functions=return_functions,
|
|
375
|
+
compute_gradients=compute_gradients,
|
|
376
|
+
split_evaluations=self._split_evaluations,
|
|
377
|
+
)
|
|
378
|
+
except Exception as err:
|
|
379
|
+
self.exception = err
|
|
380
|
+
raise
|
|
381
|
+
# Store the return value.
|
|
382
|
+
retval = {}
|
|
383
|
+
if kwargs["asv"][0] & 1:
|
|
384
|
+
retval["fns"] = function_result
|
|
385
|
+
if kwargs["asv"][0] & 2:
|
|
386
|
+
retval["fnGrads"] = gradient_result
|
|
387
|
+
return retval
|
|
388
|
+
|
|
389
|
+
def run_dakota( # noqa: PLR0913
|
|
390
|
+
self,
|
|
391
|
+
infile: str = "dakota.in",
|
|
392
|
+
stdout: Optional[str] = None,
|
|
393
|
+
stderr: Optional[str] = None,
|
|
394
|
+
restart: int = 0,
|
|
395
|
+
throw_on_error: bool = True, # noqa: FBT001,FBT002
|
|
396
|
+
) -> None:
|
|
397
|
+
overridden_infile = self._override_input_file()
|
|
398
|
+
if overridden_infile is None:
|
|
399
|
+
self.input.write_input(infile, driver_instance=self)
|
|
400
|
+
else:
|
|
401
|
+
lines = []
|
|
402
|
+
with Path(overridden_infile).open("r", encoding="utf-8") as fp_in:
|
|
403
|
+
for line in fp_in:
|
|
404
|
+
idx = line.find("analysis_components")
|
|
405
|
+
if idx >= 0:
|
|
406
|
+
ident = str(id(self))
|
|
407
|
+
_USER_DATA[ident] = self
|
|
408
|
+
lines.append(line[:idx] + f"analysis_components = '{ident}'\n")
|
|
409
|
+
else:
|
|
410
|
+
lines.append(line)
|
|
411
|
+
with Path(infile).open("w", encoding="utf-8") as fp_out:
|
|
412
|
+
fp_out.writelines(lines)
|
|
413
|
+
run_dakota(infile, stdout, stderr, restart, throw_on_error)
|
|
414
|
+
|
|
415
|
+
def _override_input_file(self) -> Optional[str]:
|
|
416
|
+
input_file = None
|
|
417
|
+
if isinstance(self._optimizer_config.options, list):
|
|
418
|
+
input_file = next(
|
|
419
|
+
(
|
|
420
|
+
option
|
|
421
|
+
for option in self._optimizer_config.options
|
|
422
|
+
if option.strip().startswith("input_file")
|
|
423
|
+
),
|
|
424
|
+
None,
|
|
425
|
+
)
|
|
426
|
+
if input_file is not None:
|
|
427
|
+
split_input_file = input_file.split("=", 1)
|
|
428
|
+
if len(split_input_file) > 1:
|
|
429
|
+
path = Path(split_input_file[1].strip())
|
|
430
|
+
if path.is_file():
|
|
431
|
+
return str(path)
|
|
432
|
+
msg = f"Invalid input_file option: {input_file}"
|
|
433
|
+
raise RuntimeError(msg)
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _compute_response( # noqa: PLR0913
|
|
438
|
+
variables: NDArray[np.float64],
|
|
439
|
+
constraint_indices: Optional[_ConstraintIndices],
|
|
440
|
+
optimizer_callback: OptimizerCallback,
|
|
441
|
+
*,
|
|
442
|
+
return_functions: bool,
|
|
443
|
+
compute_gradients: bool,
|
|
444
|
+
split_evaluations: bool,
|
|
445
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
446
|
+
if return_functions and compute_gradients and split_evaluations:
|
|
447
|
+
functions, _ = optimizer_callback(
|
|
448
|
+
variables, return_functions=True, return_gradients=False
|
|
449
|
+
)
|
|
450
|
+
_, gradients = optimizer_callback(
|
|
451
|
+
variables, return_functions=False, return_gradients=True
|
|
452
|
+
)
|
|
453
|
+
else:
|
|
454
|
+
functions, gradients = optimizer_callback(
|
|
455
|
+
variables,
|
|
456
|
+
return_functions=return_functions,
|
|
457
|
+
return_gradients=compute_gradients,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Reorder functions and gradients that correspond to nonlinear constraints:
|
|
461
|
+
if constraint_indices is not None:
|
|
462
|
+
ge_indices, le_indices, eq_indices = constraint_indices
|
|
463
|
+
indices = np.hstack((0, le_indices + 1, ge_indices + 1, eq_indices + 1))
|
|
464
|
+
if return_functions:
|
|
465
|
+
functions = functions[indices]
|
|
466
|
+
if compute_gradients:
|
|
467
|
+
gradients = gradients[indices, :]
|
|
468
|
+
return functions, gradients
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.1.0'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ropt-dakota
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Dakota optimizer plugin for ropt
|
|
5
|
+
Classifier: Development Status :: 4 - Beta
|
|
6
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
7
|
+
Classifier: Natural Language :: English
|
|
8
|
+
Classifier: Programming Language :: Python
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: carolina
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: ruff; extra == "test"
|
|
21
|
+
Requires-Dist: mypy; extra == "test"
|
|
22
|
+
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
|
|
24
|
+
# A Dakota optimizer plugin for `ropt`
|
|
25
|
+
This package installs a plugin for the `ropt` robust optimizer package, giving
|
|
26
|
+
access to algorithms from the Dakota optimization package.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
This code has been tested with Python versions 3.8, 3.9, 3.10 and 3.11.
|
|
31
|
+
|
|
32
|
+
The backend is based on the [Dakota](https://dakota.sandia.gov/) optimizer and
|
|
33
|
+
depends on the [Carolina](https://github.com/equinor/Carolina) Python wrapper.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
```bash
|
|
38
|
+
pip install ropt-dakota
|
|
39
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
README-PyPI.md
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
.github/workflows/static-checks.yml
|
|
6
|
+
.github/workflows/tests.yml
|
|
7
|
+
src/ropt_dakota/__init__.py
|
|
8
|
+
src/ropt_dakota/dakota.py
|
|
9
|
+
src/ropt_dakota/py.typed
|
|
10
|
+
src/ropt_dakota/version.py
|
|
11
|
+
src/ropt_dakota.egg-info/PKG-INFO
|
|
12
|
+
src/ropt_dakota.egg-info/SOURCES.txt
|
|
13
|
+
src/ropt_dakota.egg-info/dependency_links.txt
|
|
14
|
+
src/ropt_dakota.egg-info/entry_points.txt
|
|
15
|
+
src/ropt_dakota.egg-info/requires.txt
|
|
16
|
+
src/ropt_dakota.egg-info/top_level.txt
|
|
17
|
+
tests/__init__.py
|
|
18
|
+
tests/conftest.py
|
|
19
|
+
tests/test_dakota_backend.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ropt_dakota
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
from typing import Any, Callable, List, Tuple
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pytest
|
|
6
|
+
from numpy.typing import NDArray
|
|
7
|
+
from ropt.evaluator import Evaluator, EvaluatorContext, EvaluatorResult
|
|
8
|
+
|
|
9
|
+
_Function = Callable[[NDArray[np.float64]], float]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _function_runner(
|
|
13
|
+
variables: NDArray[np.float64],
|
|
14
|
+
metadata: EvaluatorContext,
|
|
15
|
+
functions: List[_Function],
|
|
16
|
+
) -> EvaluatorResult:
|
|
17
|
+
objective_count = metadata.config.objective_functions.weights.size
|
|
18
|
+
constraint_count = (
|
|
19
|
+
0
|
|
20
|
+
if metadata.config.nonlinear_constraints is None
|
|
21
|
+
else metadata.config.nonlinear_constraints.rhs_values.size
|
|
22
|
+
)
|
|
23
|
+
objective_results = np.zeros(
|
|
24
|
+
(variables.shape[0], objective_count), dtype=np.float64
|
|
25
|
+
)
|
|
26
|
+
constraint_results = (
|
|
27
|
+
np.zeros((variables.shape[0], constraint_count), dtype=np.float64)
|
|
28
|
+
if constraint_count > 0
|
|
29
|
+
else None
|
|
30
|
+
)
|
|
31
|
+
for sim, realization in enumerate(metadata.realizations):
|
|
32
|
+
for idx in range(objective_count):
|
|
33
|
+
if (
|
|
34
|
+
metadata.active_objectives is None
|
|
35
|
+
or metadata.active_objectives[idx, realization]
|
|
36
|
+
):
|
|
37
|
+
function = functions[idx]
|
|
38
|
+
objective_results[sim, idx] = function(variables[sim, :])
|
|
39
|
+
for idx in range(constraint_count):
|
|
40
|
+
if (
|
|
41
|
+
metadata.active_constraints is None
|
|
42
|
+
or metadata.active_constraints[idx, realization]
|
|
43
|
+
):
|
|
44
|
+
function = functions[idx + objective_count]
|
|
45
|
+
assert constraint_results is not None
|
|
46
|
+
constraint_results[sim, idx] = function(variables[sim, :])
|
|
47
|
+
return EvaluatorResult(
|
|
48
|
+
objectives=objective_results,
|
|
49
|
+
constraints=constraint_results,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _compute_distance_squared(
|
|
54
|
+
variables: NDArray[np.float64], target: NDArray[np.float64]
|
|
55
|
+
) -> float:
|
|
56
|
+
return float(((variables - target) ** 2).sum())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture(name="test_functions", scope="session")
|
|
60
|
+
def fixture_test_functions() -> Tuple[_Function, _Function]:
|
|
61
|
+
return (
|
|
62
|
+
partial(_compute_distance_squared, target=[0.5, 0.5, 0.5]),
|
|
63
|
+
partial(_compute_distance_squared, target=[-1.5, -1.5, 0.5]),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.fixture(scope="session")
|
|
68
|
+
def evaluator(test_functions: Any) -> Callable[[List[_Function]], Evaluator]:
|
|
69
|
+
def _evaluator(test_functions: List[_Function] = test_functions) -> Evaluator:
|
|
70
|
+
return partial(_function_runner, functions=test_functions)
|
|
71
|
+
|
|
72
|
+
return _evaluator
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, cast
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
from ropt.enums import ConstraintType, EventType, OptimizerExitCode
|
|
9
|
+
from ropt.events import OptimizationEvent
|
|
10
|
+
from ropt.optimization import EnsembleOptimizer
|
|
11
|
+
from ropt.results import GradientResults
|
|
12
|
+
from ropt_dakota.dakota import DakotaOptimizer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(name="enopt_config")
|
|
16
|
+
def enopt_config_fixture() -> Dict[str, Any]:
|
|
17
|
+
return {
|
|
18
|
+
"variables": {
|
|
19
|
+
"initial_values": [0.0, 0.0, 0.1],
|
|
20
|
+
},
|
|
21
|
+
"optimizer": {
|
|
22
|
+
"backend": "dakota",
|
|
23
|
+
"tolerance": 1e-6,
|
|
24
|
+
},
|
|
25
|
+
"objective_functions": {
|
|
26
|
+
"weights": [0.75, 0.25],
|
|
27
|
+
},
|
|
28
|
+
"gradient": {
|
|
29
|
+
"perturbation_magnitudes": 0.01,
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_dakota_unconstrained(enopt_config: Any, evaluator: Any) -> None:
|
|
35
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
36
|
+
result = optimizer.start_optimization(
|
|
37
|
+
plan=[
|
|
38
|
+
{"config": enopt_config},
|
|
39
|
+
{"optimizer": {"id": "opt"}},
|
|
40
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
assert result is not None
|
|
44
|
+
assert np.allclose(result.evaluations.variables, [0.0, 0.0, 0.5], atol=0.02)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_dakota_option(enopt_config: Any, evaluator: Any) -> None:
|
|
48
|
+
enopt_config["optimizer"]["options"] = ["max_iterations = 0"]
|
|
49
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
50
|
+
result = optimizer.start_optimization(
|
|
51
|
+
plan=[
|
|
52
|
+
{"config": enopt_config},
|
|
53
|
+
{"optimizer": {"id": "opt"}},
|
|
54
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
assert result is not None
|
|
58
|
+
assert np.allclose(
|
|
59
|
+
result.evaluations.variables,
|
|
60
|
+
enopt_config["variables"]["initial_values"],
|
|
61
|
+
atol=0.02,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize(
|
|
66
|
+
# The conmin algorithms are not tested, since they produce some output to
|
|
67
|
+
# the terminal that we are not able to suppress. The soga method crashes
|
|
68
|
+
# occasionally.
|
|
69
|
+
"method",
|
|
70
|
+
sorted(
|
|
71
|
+
DakotaOptimizer.SUPPORTED_ALGORITHMS - {"conmin_mfd", "conmin_frcg", "soga"}
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
def test_dakota_bound_constraint(
|
|
75
|
+
enopt_config: Any, method: str, evaluator: Any
|
|
76
|
+
) -> None:
|
|
77
|
+
enopt_config["optimizer"]["algorithm"] = method
|
|
78
|
+
enopt_config["variables"]["lower_bounds"] = -1.0
|
|
79
|
+
enopt_config["variables"]["upper_bounds"] = [1.0, 1.0, 0.2]
|
|
80
|
+
|
|
81
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
82
|
+
result = optimizer.start_optimization(
|
|
83
|
+
plan=[
|
|
84
|
+
{"config": enopt_config},
|
|
85
|
+
{"optimizer": {"id": "opt"}},
|
|
86
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
assert result is not None
|
|
90
|
+
# Some methods do not easily convert, we just test if the ran:
|
|
91
|
+
if method not in ("coliny_ea", "moga"):
|
|
92
|
+
assert np.allclose(result.evaluations.variables, [0.0, 0.0, 0.2], atol=0.02)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_dakota_eq_linear_constraint(enopt_config: Any, evaluator: Any) -> None:
|
|
96
|
+
enopt_config["linear_constraints"] = {
|
|
97
|
+
"coefficients": [[1, 0, 1], [0, 1, 1]],
|
|
98
|
+
"rhs_values": [1.0, 0.75],
|
|
99
|
+
"types": [ConstraintType.EQ, ConstraintType.EQ],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
103
|
+
result = optimizer.start_optimization(
|
|
104
|
+
plan=[
|
|
105
|
+
{"config": enopt_config},
|
|
106
|
+
{"optimizer": {"id": "opt"}},
|
|
107
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
108
|
+
],
|
|
109
|
+
)
|
|
110
|
+
assert result is not None
|
|
111
|
+
assert np.allclose(result.evaluations.variables, [0.25, 0.0, 0.75], atol=0.02)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_dakota_ge_linear_constraint(enopt_config: Any, evaluator: Any) -> None:
|
|
115
|
+
enopt_config["linear_constraints"] = {
|
|
116
|
+
"coefficients": [[-1, 0, -1]],
|
|
117
|
+
"rhs_values": -0.4,
|
|
118
|
+
"types": [ConstraintType.GE],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
122
|
+
result = optimizer.start_optimization(
|
|
123
|
+
plan=[
|
|
124
|
+
{"config": enopt_config},
|
|
125
|
+
{"optimizer": {"id": "opt"}},
|
|
126
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
assert result is not None
|
|
130
|
+
assert np.allclose(result.evaluations.variables, [-0.05, 0.0, 0.45], atol=0.02)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_dakota_le_linear_constraint(enopt_config: Any, evaluator: Any) -> None:
|
|
134
|
+
enopt_config["linear_constraints"] = {
|
|
135
|
+
"coefficients": [[1, 0, 1]],
|
|
136
|
+
"rhs_values": 0.4,
|
|
137
|
+
"types": [ConstraintType.LE],
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
141
|
+
result = optimizer.start_optimization(
|
|
142
|
+
plan=[
|
|
143
|
+
{"config": enopt_config},
|
|
144
|
+
{"optimizer": {"id": "opt"}},
|
|
145
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
146
|
+
],
|
|
147
|
+
)
|
|
148
|
+
assert result is not None
|
|
149
|
+
assert np.allclose(result.evaluations.variables, [-0.05, 0.0, 0.45], atol=0.02)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_dakota_le_ge_linear_constraints(enopt_config: Any, evaluator: Any) -> None:
|
|
153
|
+
enopt_config["linear_constraints"] = {
|
|
154
|
+
"coefficients": [[1, 0, 1], [-1, 0, -1]],
|
|
155
|
+
"rhs_values": [0.4, -0.4],
|
|
156
|
+
"types": [ConstraintType.LE, ConstraintType.GE],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
160
|
+
result = optimizer.start_optimization(
|
|
161
|
+
plan=[
|
|
162
|
+
{"config": enopt_config},
|
|
163
|
+
{"optimizer": {"id": "opt"}},
|
|
164
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
165
|
+
],
|
|
166
|
+
)
|
|
167
|
+
assert result is not None
|
|
168
|
+
assert np.allclose(result.evaluations.variables, [-0.05, 0.0, 0.45], atol=0.02)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_dakota_eq_nonlinear_constraint(
|
|
172
|
+
enopt_config: Any, evaluator: Any, test_functions: Any
|
|
173
|
+
) -> None:
|
|
174
|
+
enopt_config["nonlinear_constraints"] = {
|
|
175
|
+
"rhs_values": 1.0,
|
|
176
|
+
"types": [ConstraintType.EQ],
|
|
177
|
+
}
|
|
178
|
+
test_functions = (
|
|
179
|
+
*test_functions,
|
|
180
|
+
lambda variables: cast(NDArray[np.float64], variables[0] + variables[2]),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
optimizer = EnsembleOptimizer(evaluator(test_functions))
|
|
184
|
+
result = optimizer.start_optimization(
|
|
185
|
+
plan=[
|
|
186
|
+
{"config": enopt_config},
|
|
187
|
+
{"optimizer": {"id": "opt"}},
|
|
188
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
189
|
+
],
|
|
190
|
+
)
|
|
191
|
+
assert result is not None
|
|
192
|
+
assert np.allclose(result.evaluations.variables, [0.25, 0.0, 0.75], atol=0.02)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize("bound_type", [ConstraintType.LE, ConstraintType.GE])
|
|
196
|
+
def test_dakota_ineq_nonlinear_constraint(
|
|
197
|
+
enopt_config: Any,
|
|
198
|
+
bound_type: ConstraintType,
|
|
199
|
+
evaluator: Any,
|
|
200
|
+
test_functions: Any,
|
|
201
|
+
) -> None:
|
|
202
|
+
enopt_config["nonlinear_constraints"] = {
|
|
203
|
+
"rhs_values": 0.4 if bound_type == ConstraintType.LE else -0.4,
|
|
204
|
+
"types": [bound_type],
|
|
205
|
+
}
|
|
206
|
+
weight = 1.0 if bound_type == ConstraintType.LE else -1.0
|
|
207
|
+
test_functions = (
|
|
208
|
+
*test_functions,
|
|
209
|
+
lambda variables: cast(
|
|
210
|
+
NDArray[np.float64], weight * variables[0] + weight * variables[2]
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
optimizer = EnsembleOptimizer(evaluator(test_functions))
|
|
215
|
+
result = optimizer.start_optimization(
|
|
216
|
+
plan=[
|
|
217
|
+
{"config": enopt_config},
|
|
218
|
+
{"optimizer": {"id": "opt"}},
|
|
219
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
220
|
+
],
|
|
221
|
+
)
|
|
222
|
+
assert result is not None
|
|
223
|
+
assert np.allclose(result.evaluations.variables, [-0.05, 0.0, 0.45], atol=0.02)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_dakota_failed_realizations(enopt_config: Any, evaluator: Any) -> None:
|
|
227
|
+
def func_p(_0: NDArray[np.float64]) -> float:
|
|
228
|
+
return 1.0
|
|
229
|
+
|
|
230
|
+
def func_q(_0: NDArray[np.float64]) -> float:
|
|
231
|
+
return np.nan
|
|
232
|
+
|
|
233
|
+
functions = [func_p, func_q]
|
|
234
|
+
|
|
235
|
+
def handle_finished(event: OptimizationEvent) -> None:
|
|
236
|
+
assert event.exit_code == OptimizerExitCode.TOO_FEW_REALIZATIONS
|
|
237
|
+
|
|
238
|
+
optimizer = EnsembleOptimizer(evaluator(functions))
|
|
239
|
+
optimizer.add_observer(EventType.FINISHED_OPTIMIZER_STEP, handle_finished)
|
|
240
|
+
optimizer.start_optimization([{"config": enopt_config}, {"optimizer": {}}])
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_dakota_user_abort(enopt_config: Any, evaluator: Any) -> None:
|
|
244
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
245
|
+
|
|
246
|
+
def observer(event: OptimizationEvent, optimizer: EnsembleOptimizer) -> None:
|
|
247
|
+
assert event.results is not None
|
|
248
|
+
if event.results[0].result_id == 2:
|
|
249
|
+
optimizer.abort_optimization()
|
|
250
|
+
|
|
251
|
+
def handle_finished(event: OptimizationEvent) -> None:
|
|
252
|
+
assert event.exit_code == OptimizerExitCode.USER_ABORT
|
|
253
|
+
|
|
254
|
+
optimizer.add_observer(
|
|
255
|
+
EventType.FINISHED_EVALUATION, partial(observer, optimizer=optimizer)
|
|
256
|
+
)
|
|
257
|
+
optimizer.add_observer(EventType.FINISHED_OPTIMIZER_STEP, handle_finished)
|
|
258
|
+
result = optimizer.start_optimization(
|
|
259
|
+
plan=[
|
|
260
|
+
{"config": enopt_config},
|
|
261
|
+
{"optimizer": {"id": "opt"}},
|
|
262
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
263
|
+
],
|
|
264
|
+
)
|
|
265
|
+
assert result is not None
|
|
266
|
+
assert result.result_id == 2
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_dakota_split_evaluations(enopt_config: Any, evaluator: Any) -> None:
|
|
270
|
+
enopt_config["optimizer"]["split_evaluations"] = True
|
|
271
|
+
|
|
272
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
273
|
+
result = optimizer.start_optimization(
|
|
274
|
+
plan=[
|
|
275
|
+
{"config": enopt_config},
|
|
276
|
+
{"optimizer": {"id": "opt"}},
|
|
277
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
278
|
+
],
|
|
279
|
+
)
|
|
280
|
+
assert result is not None
|
|
281
|
+
assert np.allclose(result.evaluations.variables, [0.0, 0.0, 0.5], atol=0.02)
|
|
282
|
+
enopt_config["optimizer"]["split_evaluations"] = True
|
|
283
|
+
enopt_config["optimizer"]["speculative"] = True
|
|
284
|
+
|
|
285
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
286
|
+
result = optimizer.start_optimization(
|
|
287
|
+
plan=[
|
|
288
|
+
{"config": enopt_config},
|
|
289
|
+
{"optimizer": {"id": "opt"}},
|
|
290
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
291
|
+
],
|
|
292
|
+
)
|
|
293
|
+
assert result is not None
|
|
294
|
+
assert np.allclose(result.evaluations.variables, [0.0, 0.0, 0.5], atol=0.02)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_dakota_optimizer_variables_subset(enopt_config: Any, evaluator: Any) -> None:
|
|
298
|
+
enopt_config["variables"]["initial_values"] = [0.0, 0.0, 0.1]
|
|
299
|
+
enopt_config["variables"]["lower_bounds"] = -1.0
|
|
300
|
+
enopt_config["variables"]["upper_bounds"] = 1.0
|
|
301
|
+
|
|
302
|
+
# Fix the second variables, the test function still has the same optimal
|
|
303
|
+
# values for the other parameters:
|
|
304
|
+
enopt_config["variables"]["indices"] = [0, 2]
|
|
305
|
+
|
|
306
|
+
def assert_gradient(event: OptimizationEvent) -> None:
|
|
307
|
+
assert event.results is not None
|
|
308
|
+
for item in event.results:
|
|
309
|
+
if isinstance(item, GradientResults):
|
|
310
|
+
assert item.gradients is not None
|
|
311
|
+
assert item.gradients.weighted_objective[1] == 0.0
|
|
312
|
+
assert np.all(np.equal(item.gradients.objectives[:, 1], 0.0))
|
|
313
|
+
|
|
314
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
315
|
+
optimizer.add_observer(EventType.FINISHED_EVALUATION, assert_gradient)
|
|
316
|
+
result = optimizer.start_optimization(
|
|
317
|
+
plan=[
|
|
318
|
+
{"config": enopt_config},
|
|
319
|
+
{"optimizer": {"id": "opt"}},
|
|
320
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
321
|
+
],
|
|
322
|
+
)
|
|
323
|
+
assert result is not None
|
|
324
|
+
assert np.allclose(result.evaluations.variables, [0.0, 0.0, 0.5], atol=0.02)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def test_dakota_optimizer_variables_subset_linear_constraints(
|
|
328
|
+
enopt_config: Any, evaluator: Any
|
|
329
|
+
) -> None:
|
|
330
|
+
# Set the second variable a constant value, this will not affect the
|
|
331
|
+
# optimization of the other variables in this particular test problem:
|
|
332
|
+
enopt_config["variables"]["initial_values"] = [0.0, 1.0, 0.1]
|
|
333
|
+
# The second and third constraints are dropped because they involve
|
|
334
|
+
# variables that are not optimized. They are still checked by the monitor:
|
|
335
|
+
enopt_config["linear_constraints"] = {
|
|
336
|
+
"coefficients": [[1, 0, 1], [0, 1, 0], [1, 1, 1]],
|
|
337
|
+
"rhs_values": [1.0, 1.0, 2.0],
|
|
338
|
+
"types": [ConstraintType.EQ, ConstraintType.EQ, ConstraintType.EQ],
|
|
339
|
+
}
|
|
340
|
+
enopt_config["variables"]["indices"] = [0, 2]
|
|
341
|
+
|
|
342
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
343
|
+
result = optimizer.start_optimization(
|
|
344
|
+
plan=[
|
|
345
|
+
{"config": enopt_config},
|
|
346
|
+
{"optimizer": {"id": "opt"}},
|
|
347
|
+
{"tracker": {"id": "optimum", "source": "opt"}},
|
|
348
|
+
],
|
|
349
|
+
)
|
|
350
|
+
assert result is not None
|
|
351
|
+
assert np.allclose(result.evaluations.variables, [0.25, 1.0, 0.75], atol=0.02)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_dakota_output_dir(tmp_path: Path, enopt_config: Any, evaluator: Any) -> None:
|
|
355
|
+
output_dir = tmp_path / "outputdir"
|
|
356
|
+
output_dir.mkdir()
|
|
357
|
+
enopt_config["optimizer"]["output_dir"] = output_dir
|
|
358
|
+
enopt_config["optimizer"]["max_functions"] = 1
|
|
359
|
+
|
|
360
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
361
|
+
optimizer.start_optimization([{"config": enopt_config}, {"optimizer": {}}])
|
|
362
|
+
assert (output_dir / "dakota").exists()
|
|
363
|
+
|
|
364
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
365
|
+
optimizer.start_optimization([{"config": enopt_config}, {"optimizer": {}}])
|
|
366
|
+
assert (output_dir / "dakota-001").exists()
|
|
367
|
+
optimizer = EnsembleOptimizer(evaluator())
|
|
368
|
+
optimizer.start_optimization([{"config": enopt_config}, {"optimizer": {}}])
|
|
369
|
+
assert (output_dir / "dakota-002").exists()
|