xopt 3.0.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.
- xopt/__init__.py +35 -0
- xopt/_version.py +24 -0
- xopt/asynchronous.py +251 -0
- xopt/base.py +723 -0
- xopt/entrypoint.py +199 -0
- xopt/errors.py +42 -0
- xopt/evaluator.py +424 -0
- xopt/generator.py +178 -0
- xopt/generators/__init__.py +195 -0
- xopt/generators/bayesian/__init__.py +44 -0
- xopt/generators/bayesian/base_model.py +214 -0
- xopt/generators/bayesian/bax/__init__.py +0 -0
- xopt/generators/bayesian/bax/acquisition.py +117 -0
- xopt/generators/bayesian/bax/algorithms.py +328 -0
- xopt/generators/bayesian/bax/visualize.py +171 -0
- xopt/generators/bayesian/bax_generator.py +135 -0
- xopt/generators/bayesian/bayesian_exploration.py +134 -0
- xopt/generators/bayesian/bayesian_generator.py +1155 -0
- xopt/generators/bayesian/custom_botorch/__init__.py +0 -0
- xopt/generators/bayesian/custom_botorch/constrained_acquisition.py +105 -0
- xopt/generators/bayesian/custom_botorch/hessian_kernel.py +15 -0
- xopt/generators/bayesian/custom_botorch/heteroskedastic.py +136 -0
- xopt/generators/bayesian/custom_botorch/log_acquisition_function.py +17 -0
- xopt/generators/bayesian/custom_botorch/multi_fidelity.py +91 -0
- xopt/generators/bayesian/expected_improvement.py +175 -0
- xopt/generators/bayesian/mggpo.py +176 -0
- xopt/generators/bayesian/mobo.py +183 -0
- xopt/generators/bayesian/models/__init__.py +5 -0
- xopt/generators/bayesian/models/prior_mean.py +142 -0
- xopt/generators/bayesian/models/standard.py +682 -0
- xopt/generators/bayesian/models/time_dependent.py +168 -0
- xopt/generators/bayesian/multi_fidelity.py +325 -0
- xopt/generators/bayesian/objectives.py +190 -0
- xopt/generators/bayesian/time_dependent.py +207 -0
- xopt/generators/bayesian/turbo.py +602 -0
- xopt/generators/bayesian/upper_confidence_bound.py +132 -0
- xopt/generators/bayesian/utils.py +616 -0
- xopt/generators/bayesian/visualize.py +1467 -0
- xopt/generators/deduplicated.py +154 -0
- xopt/generators/ga/__init__.py +4 -0
- xopt/generators/ga/cnsga.py +507 -0
- xopt/generators/ga/deap_creator.py +204 -0
- xopt/generators/ga/deap_fitness_with_constraints.py +94 -0
- xopt/generators/ga/nsga2.py +749 -0
- xopt/generators/ga/operators.py +302 -0
- xopt/generators/random.py +18 -0
- xopt/generators/scipy/__init__.py +4 -0
- xopt/generators/scipy/latin_hypercube.py +147 -0
- xopt/generators/sequential/__init__.py +11 -0
- xopt/generators/sequential/extremumseeking.py +196 -0
- xopt/generators/sequential/neldermead.py +746 -0
- xopt/generators/sequential/rcds.py +817 -0
- xopt/generators/sequential/sequential_generator.py +205 -0
- xopt/generators/utils.py +111 -0
- xopt/log.py +67 -0
- xopt/mpi/__init__.py +0 -0
- xopt/mpi/run.py +98 -0
- xopt/numerical_optimizer.py +217 -0
- xopt/pydantic.py +802 -0
- xopt/resources/__init__.py +0 -0
- xopt/resources/bench_framework.py +330 -0
- xopt/resources/bench_functions/__init__.py +2 -0
- xopt/resources/bench_functions/generators.py +64 -0
- xopt/resources/bench_functions/models.py +288 -0
- xopt/resources/bench_profiler.py +47 -0
- xopt/resources/bench_runner.py +46 -0
- xopt/resources/test_functions/__init__.py +0 -0
- xopt/resources/test_functions/ackley_20.py +28 -0
- xopt/resources/test_functions/haverly_pooling.py +59 -0
- xopt/resources/test_functions/modified_tnk.py +40 -0
- xopt/resources/test_functions/multi_objective.py +175 -0
- xopt/resources/test_functions/problem.py +65 -0
- xopt/resources/test_functions/rosenbrock.py +67 -0
- xopt/resources/test_functions/sinusoid_1d.py +20 -0
- xopt/resources/test_functions/tnk.py +55 -0
- xopt/resources/test_functions/zdt.py +51 -0
- xopt/resources/testing.py +356 -0
- xopt/stopping_conditions.py +365 -0
- xopt/tests/__init__.py +0 -0
- xopt/tests/generators/__init__.py +0 -0
- xopt/tests/generators/bayesian/__init__.py +0 -0
- xopt/tests/generators/bayesian/conftest.py +35 -0
- xopt/tests/generators/bayesian/test_bax.py +357 -0
- xopt/tests/generators/bayesian/test_bax_visualize.py +88 -0
- xopt/tests/generators/bayesian/test_bayesian_exploration.py +124 -0
- xopt/tests/generators/bayesian/test_bayesian_generator.py +476 -0
- xopt/tests/generators/bayesian/test_constraints.py +83 -0
- xopt/tests/generators/bayesian/test_custom_model.py +167 -0
- xopt/tests/generators/bayesian/test_expected_improvement.py +257 -0
- xopt/tests/generators/bayesian/test_hessian_kernel.py +30 -0
- xopt/tests/generators/bayesian/test_high_level.py +223 -0
- xopt/tests/generators/bayesian/test_mggpo.py +90 -0
- xopt/tests/generators/bayesian/test_mobo.py +399 -0
- xopt/tests/generators/bayesian/test_model_constructor.py +1150 -0
- xopt/tests/generators/bayesian/test_multi_fidelity.py +140 -0
- xopt/tests/generators/bayesian/test_objectives.py +250 -0
- xopt/tests/generators/bayesian/test_time_dependent_bo.py +186 -0
- xopt/tests/generators/bayesian/test_turbo.py +594 -0
- xopt/tests/generators/bayesian/test_upper_confidence_bound.py +205 -0
- xopt/tests/generators/bayesian/test_utils.py +480 -0
- xopt/tests/generators/bayesian/test_visualize.py +292 -0
- xopt/tests/generators/external/test_aposmm.py +135 -0
- xopt/tests/generators/ga/test_cnsga.py +238 -0
- xopt/tests/generators/ga/test_deap_creator.py +76 -0
- xopt/tests/generators/ga/test_nsga2.py +988 -0
- xopt/tests/generators/sequential/test_extremum_seeking.py +303 -0
- xopt/tests/generators/sequential/test_neldermead.py +224 -0
- xopt/tests/generators/sequential/test_rcds.py +308 -0
- xopt/tests/generators/sequential/test_sequential.py +114 -0
- xopt/tests/generators/sequential/test_serialization.py +47 -0
- xopt/tests/generators/test_deduplicated.py +140 -0
- xopt/tests/generators/test_latin_hypercube.py +79 -0
- xopt/tests/generators/test_operators.py +121 -0
- xopt/tests/generators/test_random.py +21 -0
- xopt/tests/generators/test_utils.py +150 -0
- xopt/tests/test_asynch_xopt.py +515 -0
- xopt/tests/test_entrypoint.py +59 -0
- xopt/tests/test_evaluator.py +194 -0
- xopt/tests/test_generator.py +118 -0
- xopt/tests/test_io.py +34 -0
- xopt/tests/test_log.py +62 -0
- xopt/tests/test_mpi.py +100 -0
- xopt/tests/test_numerical_optimizer.py +106 -0
- xopt/tests/test_perf.py +62 -0
- xopt/tests/test_pydantic.py +673 -0
- xopt/tests/test_resources.py +108 -0
- xopt/tests/test_stopping_condition.py +566 -0
- xopt/tests/test_utils.py +415 -0
- xopt/tests/test_vocs.py +495 -0
- xopt/tests/test_xopt.py +575 -0
- xopt/utils.py +459 -0
- xopt/vocs.py +812 -0
- xopt-3.0.0.dist-info/METADATA +511 -0
- xopt-3.0.0.dist-info/RECORD +138 -0
- xopt-3.0.0.dist-info/WHEEL +5 -0
- xopt-3.0.0.dist-info/entry_points.txt +2 -0
- xopt-3.0.0.dist-info/licenses/LICENSE +201 -0
- xopt-3.0.0.dist-info/top_level.txt +1 -0
xopt/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from xopt.asynchronous import AsynchronousXopt
|
|
2
|
+
from xopt.base import Xopt
|
|
3
|
+
from xopt.evaluator import Evaluator
|
|
4
|
+
from xopt.generator import Generator
|
|
5
|
+
from xopt.vocs import VOCS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Xopt",
|
|
10
|
+
"VOCS",
|
|
11
|
+
"Generator",
|
|
12
|
+
"Evaluator",
|
|
13
|
+
"AsynchronousXopt",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from ._version import __version__
|
|
18
|
+
except ImportError:
|
|
19
|
+
__version__ = "0.0.0"
|
|
20
|
+
|
|
21
|
+
from xopt.log import configure_logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def output_notebook(**kwargs):
|
|
25
|
+
"""
|
|
26
|
+
Redirects logging to stdout for use in Jupyter notebooks
|
|
27
|
+
"""
|
|
28
|
+
configure_logger(**kwargs)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def from_file(file_path, asynchronous=False):
|
|
32
|
+
if asynchronous:
|
|
33
|
+
return AsynchronousXopt.from_file(file_path)
|
|
34
|
+
else:
|
|
35
|
+
return Xopt.from_file(file_path)
|
xopt/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '3.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 0, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
xopt/asynchronous.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import concurrent
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Dict, List, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pandas import DataFrame
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from xopt.base import logger, Xopt
|
|
11
|
+
from xopt.errors import DataError
|
|
12
|
+
from xopt.evaluator import validate_outputs
|
|
13
|
+
from xopt.vocs import validate_input_data
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsynchronousXopt(Xopt):
|
|
17
|
+
_futures: Dict = None # Will be initialized in __init__
|
|
18
|
+
_ix_last: int = 0
|
|
19
|
+
_n_unfinished_futures: int = 0
|
|
20
|
+
_input_data: DataFrame = None # Will be initialized in __init__
|
|
21
|
+
_data_lock: threading.Lock = None # Will be created lazily
|
|
22
|
+
_global_index_counter: int = 0 # Global counter for unique indices
|
|
23
|
+
is_done: bool = Field(
|
|
24
|
+
default=False, description="flag indicating that Xopt fininshed running"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def __init__(self, *args, **kwargs):
|
|
28
|
+
super().__init__(*args, **kwargs)
|
|
29
|
+
# Initialize instance-specific mutable objects
|
|
30
|
+
self._futures = {}
|
|
31
|
+
self._input_data = pd.DataFrame([])
|
|
32
|
+
self._global_index_counter = 0
|
|
33
|
+
|
|
34
|
+
def submit_data(
|
|
35
|
+
self,
|
|
36
|
+
input_data: Union[
|
|
37
|
+
pd.DataFrame,
|
|
38
|
+
List[Dict[str, float]],
|
|
39
|
+
Dict[str, List[float]],
|
|
40
|
+
Dict[str, float],
|
|
41
|
+
],
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Submit data to evaluator and return futures indexed to internal futures list.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
input_data: dataframe containing input data
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
if not isinstance(input_data, DataFrame):
|
|
53
|
+
try:
|
|
54
|
+
input_data = DataFrame(input_data)
|
|
55
|
+
except ValueError:
|
|
56
|
+
input_data = DataFrame(input_data, index=[0])
|
|
57
|
+
|
|
58
|
+
logger.debug(f"Submitting {len(input_data)} inputs")
|
|
59
|
+
input_data = self.prepare_input_data(input_data)
|
|
60
|
+
|
|
61
|
+
# submit data to evaluator. Futures are keyed on the index of the input data.
|
|
62
|
+
futures = self.evaluator.submit_data(input_data)
|
|
63
|
+
index = input_data.index
|
|
64
|
+
|
|
65
|
+
# Special handling for vectorized evaluations
|
|
66
|
+
if self.evaluator.vectorized:
|
|
67
|
+
assert len(futures) == 1
|
|
68
|
+
new_futures = {tuple(index): futures[0]}
|
|
69
|
+
else:
|
|
70
|
+
new_futures = dict(zip(index, futures))
|
|
71
|
+
|
|
72
|
+
# add futures to internal list
|
|
73
|
+
for key, future in new_futures.items():
|
|
74
|
+
assert key not in self._futures, f"{key}, {self._futures}, {future}"
|
|
75
|
+
self._futures[key] = future
|
|
76
|
+
|
|
77
|
+
return futures
|
|
78
|
+
|
|
79
|
+
def prepare_input_data(self, input_data: pd.DataFrame):
|
|
80
|
+
"""
|
|
81
|
+
re-index and validate input data.
|
|
82
|
+
"""
|
|
83
|
+
input_data = pd.DataFrame(input_data, copy=True) # copy for reindexing
|
|
84
|
+
|
|
85
|
+
# add constants to input data
|
|
86
|
+
for name, ele in self.vocs.constants.items():
|
|
87
|
+
input_data[name] = ele.value
|
|
88
|
+
|
|
89
|
+
# Reindex input dataframe
|
|
90
|
+
input_data.index = np.arange(self._ix_last, self._ix_last + len(input_data))
|
|
91
|
+
self._ix_last += len(input_data)
|
|
92
|
+
self._input_data = pd.concat([self._input_data, input_data])
|
|
93
|
+
|
|
94
|
+
# validate data before submission
|
|
95
|
+
validate_input_data(self.vocs, self._input_data)
|
|
96
|
+
|
|
97
|
+
return input_data
|
|
98
|
+
|
|
99
|
+
def step(self):
|
|
100
|
+
if self.is_done:
|
|
101
|
+
logger.debug("Xopt is done, will not step.")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# get number of candidates to generate
|
|
105
|
+
n_generate = self.evaluator.max_workers - self._n_unfinished_futures
|
|
106
|
+
|
|
107
|
+
# generate samples and submit to evaluator
|
|
108
|
+
logger.debug(f"Generating {n_generate} candidates")
|
|
109
|
+
new_samples = pd.DataFrame(self.generator.generate(n_generate))
|
|
110
|
+
|
|
111
|
+
# Submit data
|
|
112
|
+
self.submit_data(new_samples)
|
|
113
|
+
# Process futures
|
|
114
|
+
self._n_unfinished_futures = self.process_futures()
|
|
115
|
+
|
|
116
|
+
def process_futures(self):
|
|
117
|
+
logger.debug("Waiting for at least one future to complete")
|
|
118
|
+
return_when = concurrent.futures.FIRST_COMPLETED
|
|
119
|
+
|
|
120
|
+
# wait for futures to finish (depending on return_when)
|
|
121
|
+
finished_futures, unfinished_futures = concurrent.futures.wait(
|
|
122
|
+
self._futures.values(), None, return_when
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Get done indexes.
|
|
126
|
+
ix_done = [ix for ix, future in self._futures.items() if future.done()]
|
|
127
|
+
|
|
128
|
+
# Get results from futures
|
|
129
|
+
output_data = []
|
|
130
|
+
for ix in ix_done:
|
|
131
|
+
future = self._futures.pop(ix) # remove from futures
|
|
132
|
+
outputs = future.result() # Exceptions are already handled by the evaluator
|
|
133
|
+
if self.strict:
|
|
134
|
+
if future.exception() is not None:
|
|
135
|
+
raise future.exception()
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
validate_outputs(pd.DataFrame(outputs))
|
|
139
|
+
except ValueError: # handle case where outputs is a dict of lists instead of list of dicts
|
|
140
|
+
validate_outputs(pd.DataFrame(outputs, index=[1]))
|
|
141
|
+
output_data.append(outputs)
|
|
142
|
+
|
|
143
|
+
# Special handling of a vectorized futures.
|
|
144
|
+
# Dict keys have all indexes of the input data.
|
|
145
|
+
if self.evaluator.vectorized:
|
|
146
|
+
output_data = pd.concat([pd.DataFrame([output]) for output in output_data])
|
|
147
|
+
index = []
|
|
148
|
+
for ix in ix_done:
|
|
149
|
+
index.extend(list(ix))
|
|
150
|
+
else:
|
|
151
|
+
index = ix_done
|
|
152
|
+
|
|
153
|
+
# Collect done inputs and outputs
|
|
154
|
+
input_data_done = self._input_data.loc[index]
|
|
155
|
+
output_data = pd.DataFrame(output_data, index=index)
|
|
156
|
+
|
|
157
|
+
# Form completed evaluation
|
|
158
|
+
new_data = pd.concat([input_data_done, output_data], axis=1)
|
|
159
|
+
|
|
160
|
+
self.add_data(new_data)
|
|
161
|
+
|
|
162
|
+
# Cleanup
|
|
163
|
+
self._input_data.drop(index, inplace=True)
|
|
164
|
+
|
|
165
|
+
return len(unfinished_futures)
|
|
166
|
+
|
|
167
|
+
def add_data(self, new_data: pd.DataFrame):
|
|
168
|
+
"""
|
|
169
|
+
Thread-safe version of add_data for concurrent access with guaranteed unique indices.
|
|
170
|
+
|
|
171
|
+
Concatenate new data to the internal DataFrame and add it to the generator's
|
|
172
|
+
data with proper synchronization to prevent race conditions and duplicate indices.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
new_data : pd.DataFrame
|
|
177
|
+
New data to be added to the internal DataFrame.
|
|
178
|
+
"""
|
|
179
|
+
logger.debug(f"Adding {len(new_data)} new data to internal dataframes")
|
|
180
|
+
|
|
181
|
+
with self.data_lock:
|
|
182
|
+
# Set internal dataframe with thread safety and guaranteed unique indices
|
|
183
|
+
if self.data is not None:
|
|
184
|
+
new_data = pd.DataFrame(new_data, copy=True) # copy for reindexing
|
|
185
|
+
|
|
186
|
+
# Use global counter to ensure unique indices
|
|
187
|
+
start_idx = self._global_index_counter
|
|
188
|
+
new_data.index = np.arange(start_idx, start_idx + len(new_data))
|
|
189
|
+
self._global_index_counter += len(new_data)
|
|
190
|
+
|
|
191
|
+
# Double-check for uniqueness before concatenation
|
|
192
|
+
if self.data.index.intersection(new_data.index).size > 0:
|
|
193
|
+
logger.warning(
|
|
194
|
+
"Detected potential index collision, regenerating indices"
|
|
195
|
+
)
|
|
196
|
+
# Fallback: use the actual max index + 1
|
|
197
|
+
max_existing_idx = (
|
|
198
|
+
self.data.index.max() if len(self.data) > 0 else -1
|
|
199
|
+
)
|
|
200
|
+
new_data.index = np.arange(
|
|
201
|
+
max_existing_idx + 1, max_existing_idx + 1 + len(new_data)
|
|
202
|
+
)
|
|
203
|
+
self._global_index_counter = max_existing_idx + 1 + len(new_data)
|
|
204
|
+
|
|
205
|
+
self.data = pd.concat([self.data, new_data], axis=0)
|
|
206
|
+
|
|
207
|
+
# Final validation: ensure no duplicate indices
|
|
208
|
+
if not self.data.index.is_unique:
|
|
209
|
+
logger.error(
|
|
210
|
+
"Duplicate indices detected after concatenation, fixing..."
|
|
211
|
+
)
|
|
212
|
+
self.data = self.data.reset_index(drop=True)
|
|
213
|
+
self._global_index_counter = len(self.data)
|
|
214
|
+
|
|
215
|
+
else:
|
|
216
|
+
new_data = pd.DataFrame(new_data, copy=True)
|
|
217
|
+
if new_data.index.dtype != np.int64:
|
|
218
|
+
new_data.index = new_data.index.astype(np.int64)
|
|
219
|
+
# Ensure starting indices are sequential from 0
|
|
220
|
+
new_data.index = np.arange(len(new_data))
|
|
221
|
+
self._global_index_counter = len(new_data)
|
|
222
|
+
self.data = new_data
|
|
223
|
+
|
|
224
|
+
# Pass data to generator outside of lock to avoid potential deadlocks
|
|
225
|
+
# Continue in case of invalid data when strict=False
|
|
226
|
+
try:
|
|
227
|
+
self.generator.ingest(new_data.to_dict(orient="records"))
|
|
228
|
+
except DataError as exc:
|
|
229
|
+
if self.strict:
|
|
230
|
+
raise exc
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def data_lock(self):
|
|
234
|
+
"""Lazy initialization of the data lock to avoid pickling issues."""
|
|
235
|
+
if self._data_lock is None:
|
|
236
|
+
self._data_lock = threading.Lock()
|
|
237
|
+
return self._data_lock
|
|
238
|
+
|
|
239
|
+
def __getstate__(self):
|
|
240
|
+
"""Custom pickle method to exclude non-picklable threading objects."""
|
|
241
|
+
state = self.__dict__.copy()
|
|
242
|
+
# Remove the unpicklable lock
|
|
243
|
+
state["_data_lock"] = None
|
|
244
|
+
# Remove futures as they are also not picklable
|
|
245
|
+
state["_futures"] = {}
|
|
246
|
+
return state
|
|
247
|
+
|
|
248
|
+
def __setstate__(self, state):
|
|
249
|
+
"""Custom unpickle method to restore state."""
|
|
250
|
+
self.__dict__.update(state)
|
|
251
|
+
# The lock will be recreated lazily when accessed
|