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.
Files changed (138) hide show
  1. xopt/__init__.py +35 -0
  2. xopt/_version.py +24 -0
  3. xopt/asynchronous.py +251 -0
  4. xopt/base.py +723 -0
  5. xopt/entrypoint.py +199 -0
  6. xopt/errors.py +42 -0
  7. xopt/evaluator.py +424 -0
  8. xopt/generator.py +178 -0
  9. xopt/generators/__init__.py +195 -0
  10. xopt/generators/bayesian/__init__.py +44 -0
  11. xopt/generators/bayesian/base_model.py +214 -0
  12. xopt/generators/bayesian/bax/__init__.py +0 -0
  13. xopt/generators/bayesian/bax/acquisition.py +117 -0
  14. xopt/generators/bayesian/bax/algorithms.py +328 -0
  15. xopt/generators/bayesian/bax/visualize.py +171 -0
  16. xopt/generators/bayesian/bax_generator.py +135 -0
  17. xopt/generators/bayesian/bayesian_exploration.py +134 -0
  18. xopt/generators/bayesian/bayesian_generator.py +1155 -0
  19. xopt/generators/bayesian/custom_botorch/__init__.py +0 -0
  20. xopt/generators/bayesian/custom_botorch/constrained_acquisition.py +105 -0
  21. xopt/generators/bayesian/custom_botorch/hessian_kernel.py +15 -0
  22. xopt/generators/bayesian/custom_botorch/heteroskedastic.py +136 -0
  23. xopt/generators/bayesian/custom_botorch/log_acquisition_function.py +17 -0
  24. xopt/generators/bayesian/custom_botorch/multi_fidelity.py +91 -0
  25. xopt/generators/bayesian/expected_improvement.py +175 -0
  26. xopt/generators/bayesian/mggpo.py +176 -0
  27. xopt/generators/bayesian/mobo.py +183 -0
  28. xopt/generators/bayesian/models/__init__.py +5 -0
  29. xopt/generators/bayesian/models/prior_mean.py +142 -0
  30. xopt/generators/bayesian/models/standard.py +682 -0
  31. xopt/generators/bayesian/models/time_dependent.py +168 -0
  32. xopt/generators/bayesian/multi_fidelity.py +325 -0
  33. xopt/generators/bayesian/objectives.py +190 -0
  34. xopt/generators/bayesian/time_dependent.py +207 -0
  35. xopt/generators/bayesian/turbo.py +602 -0
  36. xopt/generators/bayesian/upper_confidence_bound.py +132 -0
  37. xopt/generators/bayesian/utils.py +616 -0
  38. xopt/generators/bayesian/visualize.py +1467 -0
  39. xopt/generators/deduplicated.py +154 -0
  40. xopt/generators/ga/__init__.py +4 -0
  41. xopt/generators/ga/cnsga.py +507 -0
  42. xopt/generators/ga/deap_creator.py +204 -0
  43. xopt/generators/ga/deap_fitness_with_constraints.py +94 -0
  44. xopt/generators/ga/nsga2.py +749 -0
  45. xopt/generators/ga/operators.py +302 -0
  46. xopt/generators/random.py +18 -0
  47. xopt/generators/scipy/__init__.py +4 -0
  48. xopt/generators/scipy/latin_hypercube.py +147 -0
  49. xopt/generators/sequential/__init__.py +11 -0
  50. xopt/generators/sequential/extremumseeking.py +196 -0
  51. xopt/generators/sequential/neldermead.py +746 -0
  52. xopt/generators/sequential/rcds.py +817 -0
  53. xopt/generators/sequential/sequential_generator.py +205 -0
  54. xopt/generators/utils.py +111 -0
  55. xopt/log.py +67 -0
  56. xopt/mpi/__init__.py +0 -0
  57. xopt/mpi/run.py +98 -0
  58. xopt/numerical_optimizer.py +217 -0
  59. xopt/pydantic.py +802 -0
  60. xopt/resources/__init__.py +0 -0
  61. xopt/resources/bench_framework.py +330 -0
  62. xopt/resources/bench_functions/__init__.py +2 -0
  63. xopt/resources/bench_functions/generators.py +64 -0
  64. xopt/resources/bench_functions/models.py +288 -0
  65. xopt/resources/bench_profiler.py +47 -0
  66. xopt/resources/bench_runner.py +46 -0
  67. xopt/resources/test_functions/__init__.py +0 -0
  68. xopt/resources/test_functions/ackley_20.py +28 -0
  69. xopt/resources/test_functions/haverly_pooling.py +59 -0
  70. xopt/resources/test_functions/modified_tnk.py +40 -0
  71. xopt/resources/test_functions/multi_objective.py +175 -0
  72. xopt/resources/test_functions/problem.py +65 -0
  73. xopt/resources/test_functions/rosenbrock.py +67 -0
  74. xopt/resources/test_functions/sinusoid_1d.py +20 -0
  75. xopt/resources/test_functions/tnk.py +55 -0
  76. xopt/resources/test_functions/zdt.py +51 -0
  77. xopt/resources/testing.py +356 -0
  78. xopt/stopping_conditions.py +365 -0
  79. xopt/tests/__init__.py +0 -0
  80. xopt/tests/generators/__init__.py +0 -0
  81. xopt/tests/generators/bayesian/__init__.py +0 -0
  82. xopt/tests/generators/bayesian/conftest.py +35 -0
  83. xopt/tests/generators/bayesian/test_bax.py +357 -0
  84. xopt/tests/generators/bayesian/test_bax_visualize.py +88 -0
  85. xopt/tests/generators/bayesian/test_bayesian_exploration.py +124 -0
  86. xopt/tests/generators/bayesian/test_bayesian_generator.py +476 -0
  87. xopt/tests/generators/bayesian/test_constraints.py +83 -0
  88. xopt/tests/generators/bayesian/test_custom_model.py +167 -0
  89. xopt/tests/generators/bayesian/test_expected_improvement.py +257 -0
  90. xopt/tests/generators/bayesian/test_hessian_kernel.py +30 -0
  91. xopt/tests/generators/bayesian/test_high_level.py +223 -0
  92. xopt/tests/generators/bayesian/test_mggpo.py +90 -0
  93. xopt/tests/generators/bayesian/test_mobo.py +399 -0
  94. xopt/tests/generators/bayesian/test_model_constructor.py +1150 -0
  95. xopt/tests/generators/bayesian/test_multi_fidelity.py +140 -0
  96. xopt/tests/generators/bayesian/test_objectives.py +250 -0
  97. xopt/tests/generators/bayesian/test_time_dependent_bo.py +186 -0
  98. xopt/tests/generators/bayesian/test_turbo.py +594 -0
  99. xopt/tests/generators/bayesian/test_upper_confidence_bound.py +205 -0
  100. xopt/tests/generators/bayesian/test_utils.py +480 -0
  101. xopt/tests/generators/bayesian/test_visualize.py +292 -0
  102. xopt/tests/generators/external/test_aposmm.py +135 -0
  103. xopt/tests/generators/ga/test_cnsga.py +238 -0
  104. xopt/tests/generators/ga/test_deap_creator.py +76 -0
  105. xopt/tests/generators/ga/test_nsga2.py +988 -0
  106. xopt/tests/generators/sequential/test_extremum_seeking.py +303 -0
  107. xopt/tests/generators/sequential/test_neldermead.py +224 -0
  108. xopt/tests/generators/sequential/test_rcds.py +308 -0
  109. xopt/tests/generators/sequential/test_sequential.py +114 -0
  110. xopt/tests/generators/sequential/test_serialization.py +47 -0
  111. xopt/tests/generators/test_deduplicated.py +140 -0
  112. xopt/tests/generators/test_latin_hypercube.py +79 -0
  113. xopt/tests/generators/test_operators.py +121 -0
  114. xopt/tests/generators/test_random.py +21 -0
  115. xopt/tests/generators/test_utils.py +150 -0
  116. xopt/tests/test_asynch_xopt.py +515 -0
  117. xopt/tests/test_entrypoint.py +59 -0
  118. xopt/tests/test_evaluator.py +194 -0
  119. xopt/tests/test_generator.py +118 -0
  120. xopt/tests/test_io.py +34 -0
  121. xopt/tests/test_log.py +62 -0
  122. xopt/tests/test_mpi.py +100 -0
  123. xopt/tests/test_numerical_optimizer.py +106 -0
  124. xopt/tests/test_perf.py +62 -0
  125. xopt/tests/test_pydantic.py +673 -0
  126. xopt/tests/test_resources.py +108 -0
  127. xopt/tests/test_stopping_condition.py +566 -0
  128. xopt/tests/test_utils.py +415 -0
  129. xopt/tests/test_vocs.py +495 -0
  130. xopt/tests/test_xopt.py +575 -0
  131. xopt/utils.py +459 -0
  132. xopt/vocs.py +812 -0
  133. xopt-3.0.0.dist-info/METADATA +511 -0
  134. xopt-3.0.0.dist-info/RECORD +138 -0
  135. xopt-3.0.0.dist-info/WHEEL +5 -0
  136. xopt-3.0.0.dist-info/entry_points.txt +2 -0
  137. xopt-3.0.0.dist-info/licenses/LICENSE +201 -0
  138. 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