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.
@@ -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,3 @@
1
+ **/__pycache__/**
2
+ src/**egg-info/**
3
+ src/ropt_dakota/version.py
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [ropt.plugins.optimizer]
2
+ dakota = ropt_dakota.dakota:DakotaOptimizer
@@ -0,0 +1,7 @@
1
+ numpy
2
+ carolina
3
+
4
+ [test]
5
+ ruff
6
+ mypy
7
+ pytest
@@ -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()