ropt 0.1.0__py3-none-any.whl
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/__init__.py +1 -0
- ropt/apps/__init__.py +7 -0
- ropt/apps/_script_optimizer.py +463 -0
- ropt/config/__init__.py +1 -0
- ropt/config/enopt/__init__.py +27 -0
- ropt/config/enopt/_enopt_base_model.py +15 -0
- ropt/config/enopt/_enopt_config.py +208 -0
- ropt/config/enopt/_function_transform_config.py +43 -0
- ropt/config/enopt/_gradient_config.py +119 -0
- ropt/config/enopt/_linear_constraints_config.py +83 -0
- ropt/config/enopt/_nonlinear_constraints_config.py +112 -0
- ropt/config/enopt/_objective_functions_config.py +100 -0
- ropt/config/enopt/_optimizer_config.py +84 -0
- ropt/config/enopt/_realization_filter_config.py +46 -0
- ropt/config/enopt/_realizations_config.py +85 -0
- ropt/config/enopt/_sampler_config.py +52 -0
- ropt/config/enopt/_utils.py +132 -0
- ropt/config/enopt/_variables_config.py +184 -0
- ropt/config/enopt/constants.py +36 -0
- ropt/config/plan/__init__.py +26 -0
- ropt/config/plan/_config.py +156 -0
- ropt/config/plan/_plan_config.py +21 -0
- ropt/config/plan/_step_config.py +27 -0
- ropt/config/utils.py +185 -0
- ropt/enums.py +175 -0
- ropt/evaluator/__init__.py +50 -0
- ropt/evaluator/_concurrent.py +189 -0
- ropt/evaluator/_ensemble_evaluator.py +653 -0
- ropt/evaluator/_evaluator.py +143 -0
- ropt/evaluator/_evaluator_results.py +244 -0
- ropt/evaluator/_function.py +69 -0
- ropt/evaluator/_gradient.py +229 -0
- ropt/evaluator/_utils.py +25 -0
- ropt/evaluator/parsl.py +161 -0
- ropt/events.py +39 -0
- ropt/exceptions.py +25 -0
- ropt/optimization/__init__.py +18 -0
- ropt/optimization/_bases.py +122 -0
- ropt/optimization/_ensemble_optimizer.py +118 -0
- ropt/optimization/_events.py +27 -0
- ropt/optimization/_optimizer.py +246 -0
- ropt/optimization/_plan.py +218 -0
- ropt/plugins/__init__.py +39 -0
- ropt/plugins/_manager.py +156 -0
- ropt/plugins/function_transform/__init__.py +5 -0
- ropt/plugins/function_transform/default.py +152 -0
- ropt/plugins/function_transform/protocol.py +72 -0
- ropt/plugins/optimization_steps/__init__.py +7 -0
- ropt/plugins/optimization_steps/_utils.py +91 -0
- ropt/plugins/optimization_steps/default.py +58 -0
- ropt/plugins/optimization_steps/enopt_config.py +37 -0
- ropt/plugins/optimization_steps/evaluator.py +112 -0
- ropt/plugins/optimization_steps/label.py +29 -0
- ropt/plugins/optimization_steps/optimizer.py +139 -0
- ropt/plugins/optimization_steps/reset_tracker.py +34 -0
- ropt/plugins/optimization_steps/restart.py +57 -0
- ropt/plugins/optimization_steps/tracker.py +88 -0
- ropt/plugins/optimization_steps/update_config.py +51 -0
- ropt/plugins/optimizer/__init__.py +21 -0
- ropt/plugins/optimizer/protocol.py +105 -0
- ropt/plugins/optimizer/scipy.py +557 -0
- ropt/plugins/optimizer/utils.py +247 -0
- ropt/plugins/realization_filter/__init__.py +11 -0
- ropt/plugins/realization_filter/default.py +384 -0
- ropt/plugins/realization_filter/protocol.py +58 -0
- ropt/plugins/sampler/__init__.py +14 -0
- ropt/plugins/sampler/protocol.py +87 -0
- ropt/plugins/sampler/scipy.py +199 -0
- ropt/py.typed +0 -0
- ropt/report/__init__.py +16 -0
- ropt/report/_data_frame.py +212 -0
- ropt/report/_table.py +127 -0
- ropt/report/_utils.py +116 -0
- ropt/results/__init__.py +85 -0
- ropt/results/_bound_constraints.py +174 -0
- ropt/results/_function_evaluations.py +161 -0
- ropt/results/_function_results.py +103 -0
- ropt/results/_functions.py +120 -0
- ropt/results/_gradient_evaluations.py +95 -0
- ropt/results/_gradient_results.py +90 -0
- ropt/results/_gradients.py +61 -0
- ropt/results/_linear_constraints.py +87 -0
- ropt/results/_maximize.py +64 -0
- ropt/results/_nonlinear_constraints.py +129 -0
- ropt/results/_pandas.py +104 -0
- ropt/results/_realizations.py +67 -0
- ropt/results/_result_field.py +40 -0
- ropt/results/_results.py +227 -0
- ropt/results/_utils.py +104 -0
- ropt/results/_xarray.py +131 -0
- ropt/utils/__init__.py +9 -0
- ropt/utils/_misc.py +27 -0
- ropt/utils/scaling.py +147 -0
- ropt/version.py +16 -0
- ropt-0.1.0.dist-info/LICENSE +674 -0
- ropt-0.1.0.dist-info/METADATA +82 -0
- ropt-0.1.0.dist-info/RECORD +99 -0
- ropt-0.1.0.dist-info/WHEEL +5 -0
- ropt-0.1.0.dist-info/top_level.txt +1 -0
ropt/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The main `ropt` module, a library for ensemble based optimization."""
|
ropt/apps/__init__.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""A class for running optimizations with script-based jobs."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from collections import abc, defaultdict
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from itertools import groupby
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from string import Template
|
|
11
|
+
from traceback import format_exception
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
DefaultDict,
|
|
16
|
+
Dict,
|
|
17
|
+
List,
|
|
18
|
+
Optional,
|
|
19
|
+
Sequence,
|
|
20
|
+
TextIO,
|
|
21
|
+
Tuple,
|
|
22
|
+
Union,
|
|
23
|
+
no_type_check,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
from numpy.typing import NDArray
|
|
28
|
+
from parsl.app.app import bash_app
|
|
29
|
+
from parsl.providers.base import ExecutionProvider
|
|
30
|
+
from tabulate import tabulate
|
|
31
|
+
|
|
32
|
+
from ropt.config.enopt import EnOptConfig
|
|
33
|
+
from ropt.enums import EventType, OptimizerExitCode
|
|
34
|
+
from ropt.evaluator import EvaluatorContext
|
|
35
|
+
from ropt.evaluator.parsl import ParslEvaluator, State, Task
|
|
36
|
+
from ropt.events import OptimizationEvent
|
|
37
|
+
from ropt.exceptions import ConfigError
|
|
38
|
+
from ropt.optimization import EnsembleOptimizer
|
|
39
|
+
from ropt.results import FunctionResults
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _make_dict(
|
|
43
|
+
variables: NDArray[np.float64], names: Sequence[Tuple[str, ...]]
|
|
44
|
+
) -> DefaultDict[str, Any]:
|
|
45
|
+
def recursive_dict() -> DefaultDict[str, Any]:
|
|
46
|
+
return defaultdict(recursive_dict)
|
|
47
|
+
|
|
48
|
+
var_dict = recursive_dict()
|
|
49
|
+
for idx, var_name in enumerate(names):
|
|
50
|
+
tmp = var_dict
|
|
51
|
+
for name in var_name[:-1]:
|
|
52
|
+
tmp = tmp[str(name)]
|
|
53
|
+
tmp[str(var_name[-1])] = variables[idx]
|
|
54
|
+
|
|
55
|
+
return var_dict
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _write_dict(var_dict: Dict[str, Any], path: Path, name: str, depth: int) -> None:
|
|
59
|
+
if depth == 0 or not isinstance(var_dict, abc.Mapping):
|
|
60
|
+
filename = (path / name if name else path / "variables").with_suffix(".json")
|
|
61
|
+
with filename.open("w", encoding="utf-8") as file_obj:
|
|
62
|
+
json.dump(var_dict, file_obj, indent=2)
|
|
63
|
+
else:
|
|
64
|
+
for key, value in var_dict.items():
|
|
65
|
+
_write_dict(value, path, f"{name}_{key}" if name else key, depth - 1)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def variables_to_json(
|
|
69
|
+
variables: NDArray[np.float64], names: Tuple[Any, ...], path: Path, depth: int = 0
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Export a vector of variables with given names to json.
|
|
72
|
+
|
|
73
|
+
The `names` parameter must provide the name of each variable stored in
|
|
74
|
+
variables. Each name should be a tuple, which will be used to generate a
|
|
75
|
+
nested dictionary entry with the variable value. The generated dict is
|
|
76
|
+
stored in one or more json files depending on the value of `depth`. If
|
|
77
|
+
`depth == 0`, the dictionary is saved as a single file with the name
|
|
78
|
+
`variables.json`. If `depth > 0` files are generated by generating a json
|
|
79
|
+
file for each entry given by the first `depth`th components of the name. The
|
|
80
|
+
name of each file is constructed by concatenating the first `depth`th name
|
|
81
|
+
components.
|
|
82
|
+
|
|
83
|
+
The `level` parameter must be at least zero. The `path` parameters must
|
|
84
|
+
denote an existing directory.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
variables: The variables to export
|
|
88
|
+
names: The names of the variables
|
|
89
|
+
path: The directory to store the file
|
|
90
|
+
depth: The depth at which to generate files
|
|
91
|
+
"""
|
|
92
|
+
msg = ""
|
|
93
|
+
if depth < 0:
|
|
94
|
+
msg = "The depth parameter must be at least zero"
|
|
95
|
+
if not path.is_dir():
|
|
96
|
+
msg = "The path parameters should point to a valid directory"
|
|
97
|
+
if variables.ndim != 1:
|
|
98
|
+
msg = "The variables parameter must be a 1D vector"
|
|
99
|
+
if len(names) != variables.size:
|
|
100
|
+
msg = "The length of the names parameter must match the variables size"
|
|
101
|
+
if msg:
|
|
102
|
+
raise ValueError(msg)
|
|
103
|
+
return _write_dict(_make_dict(variables, names), path, "", depth)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class ScriptTask(Task):
|
|
108
|
+
"""Task class for use by the ScriptBasedOptimizer class."""
|
|
109
|
+
|
|
110
|
+
name: str = ""
|
|
111
|
+
job_labels: Tuple[str, ...] = field(default_factory=tuple)
|
|
112
|
+
objective_paths: Tuple[Path, ...] = field(default_factory=tuple)
|
|
113
|
+
constraint_paths: Tuple[Path, ...] = field(default_factory=tuple)
|
|
114
|
+
logged: bool = False
|
|
115
|
+
realization: int = 0
|
|
116
|
+
_objectives: Optional[NDArray[np.float64]] = None
|
|
117
|
+
_constraints: Optional[NDArray[np.float64]] = None
|
|
118
|
+
|
|
119
|
+
def _read_file(self, path: Path) -> float:
|
|
120
|
+
try:
|
|
121
|
+
with path.open("r", encoding="utf-8") as file_obj:
|
|
122
|
+
return float(file_obj.read())
|
|
123
|
+
except BaseException as exc: # noqa: BLE001
|
|
124
|
+
self.exception = exc
|
|
125
|
+
return np.nan
|
|
126
|
+
|
|
127
|
+
def _read_results(self) -> None:
|
|
128
|
+
if self.objective_paths:
|
|
129
|
+
self._objectives = np.zeros(len(self.objective_paths), dtype=np.float64)
|
|
130
|
+
for idx, path in enumerate(self.objective_paths):
|
|
131
|
+
self._objectives[idx] = self._read_file(path)
|
|
132
|
+
if self.constraint_paths:
|
|
133
|
+
self._constraints = np.zeros(len(self.constraint_paths), dtype=np.float64)
|
|
134
|
+
for idx, path in enumerate(self.constraint_paths):
|
|
135
|
+
self._constraints[idx] = self._read_file(path)
|
|
136
|
+
|
|
137
|
+
def get_objectives(self) -> Optional[NDArray[np.float64]]:
|
|
138
|
+
"""Get the objective values.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The objective values.
|
|
142
|
+
"""
|
|
143
|
+
self._read_results()
|
|
144
|
+
return self._objectives
|
|
145
|
+
|
|
146
|
+
def get_constraints(self) -> Optional[NDArray[np.float64]]:
|
|
147
|
+
"""Get the constraint values.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The constraint values.
|
|
151
|
+
"""
|
|
152
|
+
self._read_results()
|
|
153
|
+
return self._constraints
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@no_type_check
|
|
157
|
+
@bash_app()
|
|
158
|
+
def run_script(
|
|
159
|
+
_: Any, # noqa: ANN401
|
|
160
|
+
script: str,
|
|
161
|
+
stdout: TextIO, # noqa: ARG001
|
|
162
|
+
stderr: TextIO, # noqa: ARG001
|
|
163
|
+
) -> str:
|
|
164
|
+
return script
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_function_files(config: EnOptConfig) -> Tuple[Tuple[str, ...], Tuple[str, ...]]:
|
|
168
|
+
"""Default function to retrieve names of the files with evaluation results.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
config: Optimizer configuration file.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Two tuples with filenames of objective and constraint functions.
|
|
175
|
+
"""
|
|
176
|
+
assert config.objective_functions.names is not None
|
|
177
|
+
objective_names = tuple(str(name) for name in config.objective_functions.names)
|
|
178
|
+
if config.nonlinear_constraints is not None:
|
|
179
|
+
assert config.nonlinear_constraints.names is not None
|
|
180
|
+
constraint_names = tuple(
|
|
181
|
+
str(name) for name in config.nonlinear_constraints.names
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
constraint_names = ()
|
|
185
|
+
return objective_names, constraint_names
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ScriptOptimizer:
|
|
189
|
+
"""Optimizer class for running script bases optimization workflows."""
|
|
190
|
+
|
|
191
|
+
def __init__( # noqa: PLR0913
|
|
192
|
+
self,
|
|
193
|
+
plan: Sequence[Dict[str, Any]],
|
|
194
|
+
tasks: Dict[str, str],
|
|
195
|
+
work_dir: Union[Path, str],
|
|
196
|
+
*,
|
|
197
|
+
job_dir: Optional[Union[Path, str]] = None,
|
|
198
|
+
job_labels: Tuple[str, ...] = ("B{batch:04d}", "J{job:04d}"),
|
|
199
|
+
variable_export_depth: int = 0,
|
|
200
|
+
get_function_files: Callable[
|
|
201
|
+
[EnOptConfig], Tuple[Tuple[str, ...], Tuple[str, ...]]
|
|
202
|
+
] = get_function_files,
|
|
203
|
+
callbacks: Optional[Dict[EventType, Callable[..., None]]] = None,
|
|
204
|
+
seed: Optional[int] = None,
|
|
205
|
+
provider: Optional[ExecutionProvider] = None,
|
|
206
|
+
max_threads: int = 4,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Initialize the optimizer.
|
|
209
|
+
|
|
210
|
+
The directory where the jobs run is constructed from by nesting
|
|
211
|
+
directories according the `job_labels` tuple. These directories are
|
|
212
|
+
located in the directory where the optimizer runs, which can be set
|
|
213
|
+
using the `work_dir` parameter of the `run()` method.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
plan: The optimization plan to run
|
|
217
|
+
tasks: A dictionary mapping task names to strings
|
|
218
|
+
containing bash code
|
|
219
|
+
work_dir: Working directory
|
|
220
|
+
job_dir: The directory to store files generated during
|
|
221
|
+
optimization
|
|
222
|
+
job_labels: Label formats to use in generating filenames
|
|
223
|
+
reports
|
|
224
|
+
variable_export_depth: The depth parameter for exporting variables
|
|
225
|
+
get_function_files: Callable to retrieve function file names
|
|
226
|
+
callbacks: Dictionary of callbacks for the optimizer
|
|
227
|
+
seed: Seed for the random number generator
|
|
228
|
+
provider: The provider that executes the jobs
|
|
229
|
+
max_threads: Maximum number of threads for local runs
|
|
230
|
+
"""
|
|
231
|
+
self._plan = plan
|
|
232
|
+
self._tasks = tasks
|
|
233
|
+
self._work_dir = Path(work_dir).resolve()
|
|
234
|
+
self._job_labels = job_labels
|
|
235
|
+
self._variable_export_depth = variable_export_depth
|
|
236
|
+
self._get_function_files = get_function_files
|
|
237
|
+
self._callbacks = callbacks
|
|
238
|
+
self._seed = seed
|
|
239
|
+
self._provider = provider
|
|
240
|
+
self._max_threads = max_threads
|
|
241
|
+
self._status: Dict[int, Any] = {}
|
|
242
|
+
self._optimal_result: Optional[FunctionResults] = None
|
|
243
|
+
|
|
244
|
+
self._job_dir = self._work_dir if job_dir is None else Path(job_dir)
|
|
245
|
+
if not self._job_dir.is_absolute():
|
|
246
|
+
self._job_dir = self._work_dir / self._job_dir
|
|
247
|
+
|
|
248
|
+
def _set_logger(self) -> None:
|
|
249
|
+
self._logger = logging.getLogger("ScriptBasedOptimizer")
|
|
250
|
+
self._logger.setLevel(level=logging.INFO)
|
|
251
|
+
handler = logging.FileHandler("optimizer.log")
|
|
252
|
+
formatter = logging.Formatter("%(levelname)s: %(message)s")
|
|
253
|
+
handler.setFormatter(formatter)
|
|
254
|
+
self._logger.addHandler(handler)
|
|
255
|
+
|
|
256
|
+
def _workflow(
|
|
257
|
+
self,
|
|
258
|
+
batch_id: int,
|
|
259
|
+
job_idx: int,
|
|
260
|
+
variables: NDArray[np.float64],
|
|
261
|
+
context: EvaluatorContext,
|
|
262
|
+
) -> List[ScriptTask]:
|
|
263
|
+
assert context.config.realizations.names is not None
|
|
264
|
+
realization = context.config.realizations.names[context.realizations[job_idx]]
|
|
265
|
+
if not isinstance(realization, int):
|
|
266
|
+
msg = f"Realization name must be an integer: {realization}"
|
|
267
|
+
raise ConfigError(msg)
|
|
268
|
+
|
|
269
|
+
job_labels = tuple(
|
|
270
|
+
label.format(batch=batch_id, realization=realization, job=job_idx)
|
|
271
|
+
for label in self._job_labels
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
path = self._job_dir / Path(*job_labels)
|
|
275
|
+
Path.mkdir(path, parents=True, exist_ok=True)
|
|
276
|
+
|
|
277
|
+
assert context.config.variables.names is not None
|
|
278
|
+
variables_to_json(
|
|
279
|
+
variables[job_idx],
|
|
280
|
+
context.config.variables.names,
|
|
281
|
+
path,
|
|
282
|
+
depth=self._variable_export_depth,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
tasks: List[ScriptTask] = []
|
|
286
|
+
for task_name, script in self._tasks.items():
|
|
287
|
+
substituted_script = Template(script).safe_substitute(
|
|
288
|
+
work_dir=self._work_dir, realization=realization
|
|
289
|
+
)
|
|
290
|
+
tasks.append(
|
|
291
|
+
ScriptTask(
|
|
292
|
+
name=task_name,
|
|
293
|
+
future=run_script(
|
|
294
|
+
tasks[-1].future if tasks else None,
|
|
295
|
+
f"cd {path}\n{substituted_script}",
|
|
296
|
+
stdout=((path / task_name).with_suffix(".stdout"), "a"),
|
|
297
|
+
stderr=((path / task_name).with_suffix(".stderr"), "a"),
|
|
298
|
+
),
|
|
299
|
+
job_labels=job_labels,
|
|
300
|
+
realization=realization,
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
objective_names, constraint_names = self._get_function_files(context.config)
|
|
305
|
+
tasks[-1].objective_paths = tuple(path / name for name in objective_names)
|
|
306
|
+
tasks[-1].constraint_paths = tuple(path / name for name in constraint_names)
|
|
307
|
+
|
|
308
|
+
return tasks
|
|
309
|
+
|
|
310
|
+
def _log_task(self, task: ScriptTask) -> None:
|
|
311
|
+
if not task.logged:
|
|
312
|
+
job_label = ", ".join(task.job_labels)
|
|
313
|
+
|
|
314
|
+
if task.state == State.FAILED:
|
|
315
|
+
assert task.exception is not None
|
|
316
|
+
msg = f"{job_label}, {task.name}: FAILED\n"
|
|
317
|
+
msg += "".join(
|
|
318
|
+
format_exception(
|
|
319
|
+
type(task.exception),
|
|
320
|
+
task.exception,
|
|
321
|
+
task.exception.__traceback__,
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
self._logger.error(msg)
|
|
325
|
+
task.logged = True
|
|
326
|
+
|
|
327
|
+
if task.state == State.SUCCESS:
|
|
328
|
+
msg = f"{job_label}, {task.name}: FINISHED"
|
|
329
|
+
self._logger.info(msg)
|
|
330
|
+
task.logged = True
|
|
331
|
+
|
|
332
|
+
def _write_state_report(
|
|
333
|
+
self, batch_id: int, jobs: Dict[int, List[ScriptTask]]
|
|
334
|
+
) -> None:
|
|
335
|
+
states: DefaultDict[str, Dict[int, State]] = defaultdict(dict)
|
|
336
|
+
for job_idx, tasks in jobs.items():
|
|
337
|
+
for task in tasks:
|
|
338
|
+
states[task.name][job_idx] = task.state
|
|
339
|
+
table: List[Dict[str, Any]] = []
|
|
340
|
+
for name, task_states in states.items():
|
|
341
|
+
job_name = name
|
|
342
|
+
for state in State:
|
|
343
|
+
job_indices = [
|
|
344
|
+
idx for idx, item in task_states.items() if item == state
|
|
345
|
+
]
|
|
346
|
+
if job_indices:
|
|
347
|
+
table.append(
|
|
348
|
+
{
|
|
349
|
+
"Task": job_name,
|
|
350
|
+
"State": state.value,
|
|
351
|
+
"Jobs": _format_list(job_indices),
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
job_name = ""
|
|
355
|
+
with (self._work_dir / "status.txt").open("w", encoding="utf-8") as file_obj:
|
|
356
|
+
file_obj.write(f"Batch: {batch_id}\n\n")
|
|
357
|
+
table_str = tabulate(table, headers="keys", tablefmt="simple")
|
|
358
|
+
file_obj.write(f"{table_str}\n")
|
|
359
|
+
|
|
360
|
+
def _update_current_state(
|
|
361
|
+
self, batch_id: int, jobs: Dict[int, List[ScriptTask]]
|
|
362
|
+
) -> None:
|
|
363
|
+
# Update the current batch
|
|
364
|
+
states: DefaultDict[int, Dict[str, Any]] = defaultdict(dict)
|
|
365
|
+
for job_idx, tasks in jobs.items():
|
|
366
|
+
for task in tasks:
|
|
367
|
+
states[job_idx][task.name] = {
|
|
368
|
+
"state": task.state.value,
|
|
369
|
+
"realization": task.realization,
|
|
370
|
+
}
|
|
371
|
+
self._log_task(task)
|
|
372
|
+
self._status[batch_id] = states
|
|
373
|
+
|
|
374
|
+
def _store_states(self) -> None:
|
|
375
|
+
states_json = []
|
|
376
|
+
for batch_id in sorted(self._status.keys()):
|
|
377
|
+
states = self._status[batch_id]
|
|
378
|
+
states_json.append(
|
|
379
|
+
{
|
|
380
|
+
"batch_id": batch_id,
|
|
381
|
+
"jobs": [
|
|
382
|
+
{
|
|
383
|
+
"job": job_idx,
|
|
384
|
+
"tasks": [
|
|
385
|
+
{
|
|
386
|
+
"name": task_name,
|
|
387
|
+
"state": task_state["state"],
|
|
388
|
+
"realization": task_state["realization"],
|
|
389
|
+
}
|
|
390
|
+
for task_name, task_state in states[job_idx].items()
|
|
391
|
+
],
|
|
392
|
+
}
|
|
393
|
+
for job_idx in sorted(self._status[batch_id])
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
with (self._work_dir / "states.json").open("w", encoding="utf-8") as file_obj:
|
|
398
|
+
json.dump(states_json, file_obj, sort_keys=True, indent=4)
|
|
399
|
+
|
|
400
|
+
def _monitor(self, batch_id: int, jobs: Dict[int, List[ScriptTask]]) -> None:
|
|
401
|
+
self._write_state_report(batch_id, jobs)
|
|
402
|
+
self._update_current_state(batch_id, jobs)
|
|
403
|
+
self._store_states()
|
|
404
|
+
|
|
405
|
+
def _log_exit_code(self, event: OptimizationEvent) -> None:
|
|
406
|
+
exit_code = event.exit_code
|
|
407
|
+
assert event.config is not None
|
|
408
|
+
if exit_code == OptimizerExitCode.TOO_FEW_REALIZATIONS:
|
|
409
|
+
self._logger.warning(
|
|
410
|
+
"Too few successful realizations: optimization stopped.",
|
|
411
|
+
)
|
|
412
|
+
elif exit_code == OptimizerExitCode.MAX_FUNCTIONS_REACHED:
|
|
413
|
+
self._logger.warning(
|
|
414
|
+
"Maximum number of functions reached: optimization stopped.",
|
|
415
|
+
)
|
|
416
|
+
elif exit_code == OptimizerExitCode.USER_ABORT:
|
|
417
|
+
self._logger.warning("Optimization aborted by the user.")
|
|
418
|
+
elif exit_code == OptimizerExitCode.OPTIMIZER_STEP_FINISHED:
|
|
419
|
+
self._logger.info("Optimization finished normally.")
|
|
420
|
+
|
|
421
|
+
def _handle_finished_optimizer(self, event: OptimizationEvent) -> None:
|
|
422
|
+
self._log_exit_code(event)
|
|
423
|
+
|
|
424
|
+
def run(self) -> Dict[str, Optional[FunctionResults]]:
|
|
425
|
+
"""Run the optimization."""
|
|
426
|
+
cwd = Path.cwd()
|
|
427
|
+
Path.mkdir(self._work_dir, parents=True, exist_ok=True)
|
|
428
|
+
os.chdir(self._work_dir)
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
self._set_logger()
|
|
432
|
+
evaluator = ParslEvaluator(
|
|
433
|
+
self._workflow,
|
|
434
|
+
monitor=self._monitor,
|
|
435
|
+
provider=self._provider,
|
|
436
|
+
max_threads=self._max_threads,
|
|
437
|
+
)
|
|
438
|
+
optimizer = EnsembleOptimizer(evaluator)
|
|
439
|
+
optimizer.add_observer(
|
|
440
|
+
EventType.FINISHED_OPTIMIZER_STEP,
|
|
441
|
+
self._handle_finished_optimizer,
|
|
442
|
+
)
|
|
443
|
+
if self._callbacks is not None:
|
|
444
|
+
for event, callback in self._callbacks.items():
|
|
445
|
+
optimizer.add_observer(event, callback)
|
|
446
|
+
optimizer.start_optimization(self._plan, seed=self._seed)
|
|
447
|
+
finally:
|
|
448
|
+
os.chdir(cwd)
|
|
449
|
+
|
|
450
|
+
return optimizer.results
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _format_list(values: List[int]) -> str:
|
|
454
|
+
grouped = (
|
|
455
|
+
tuple(y for _, y in x)
|
|
456
|
+
for _, x in groupby(enumerate(sorted(values)), lambda x: x[0] - x[1])
|
|
457
|
+
)
|
|
458
|
+
return ", ".join(
|
|
459
|
+
"-".join([str(sub_group[0]), str(sub_group[-1])])
|
|
460
|
+
if len(sub_group) > 1
|
|
461
|
+
else str(sub_group[0])
|
|
462
|
+
for sub_group in grouped
|
|
463
|
+
)
|
ropt/config/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The `ropt.config` module contains configuration related code."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""The `ropt.config.enopt` module contains optimization configuration classes."""
|
|
2
|
+
|
|
3
|
+
from ._enopt_config import EnOptConfig
|
|
4
|
+
from ._function_transform_config import FunctionTransformConfig
|
|
5
|
+
from ._gradient_config import GradientConfig
|
|
6
|
+
from ._linear_constraints_config import LinearConstraintsConfig
|
|
7
|
+
from ._nonlinear_constraints_config import NonlinearConstraintsConfig
|
|
8
|
+
from ._objective_functions_config import ObjectiveFunctionsConfig
|
|
9
|
+
from ._optimizer_config import OptimizerConfig
|
|
10
|
+
from ._realization_filter_config import RealizationFilterConfig
|
|
11
|
+
from ._realizations_config import RealizationsConfig
|
|
12
|
+
from ._sampler_config import SamplerConfig
|
|
13
|
+
from ._variables_config import VariablesConfig
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"EnOptConfig",
|
|
17
|
+
"FunctionTransformConfig",
|
|
18
|
+
"GradientConfig",
|
|
19
|
+
"LinearConstraintsConfig",
|
|
20
|
+
"NonlinearConstraintsConfig",
|
|
21
|
+
"ObjectiveFunctionsConfig",
|
|
22
|
+
"OptimizerConfig",
|
|
23
|
+
"RealizationFilterConfig",
|
|
24
|
+
"RealizationsConfig",
|
|
25
|
+
"SamplerConfig",
|
|
26
|
+
"VariablesConfig",
|
|
27
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Base model class for `EnOptConfig` fields."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EnOptBaseModel(BaseModel):
|
|
9
|
+
model_config = ConfigDict(
|
|
10
|
+
arbitrary_types_allowed=True,
|
|
11
|
+
extra="forbid",
|
|
12
|
+
str_min_length=1,
|
|
13
|
+
str_strip_whitespace=True,
|
|
14
|
+
validate_default=True,
|
|
15
|
+
)
|