xopt 2.2.2__tar.gz → 2.3.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.
- {xopt-2.2.2/xopt.egg-info → xopt-2.3.0}/PKG-INFO +1 -1
- {xopt-2.2.2 → xopt-2.3.0}/requirements.txt +1 -1
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_bayesian_generator.py +11 -2
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_expected_improvement.py +23 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_time_dependent_bo.py +1 -1
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_utils.py +1 -1
- {xopt-2.2.2 → xopt-2.3.0}/tests/test_evaluator.py +28 -1
- {xopt-2.2.2 → xopt-2.3.0}/tests/test_vocs.py +46 -2
- {xopt-2.2.2 → xopt-2.3.0}/xopt/_version.py +3 -3
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generator.py +1 -1
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/__init__.py +24 -28
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bax/algorithms.py +42 -5
- xopt-2.3.0/xopt/generators/bayesian/bax/visualize.py +171 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bayesian_exploration.py +0 -7
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bayesian_generator.py +27 -4
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/expected_improvement.py +25 -7
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/mggpo.py +1 -1
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/mobo.py +1 -1
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/objectives.py +47 -22
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/upper_confidence_bound.py +4 -4
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/utils.py +20 -7
- xopt-2.3.0/xopt/generators/bayesian/visualize.py +1095 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/scipy/neldermead.py +2 -3
- xopt-2.3.0/xopt/resources/test_functions/haverly_pooling.py +59 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/utils.py +5 -1
- {xopt-2.2.2 → xopt-2.3.0}/xopt/vocs.py +60 -2
- {xopt-2.2.2 → xopt-2.3.0/xopt.egg-info}/PKG-INFO +1 -1
- {xopt-2.2.2 → xopt-2.3.0}/xopt.egg-info/SOURCES.txt +2 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt.egg-info/requires.txt +1 -1
- xopt-2.2.2/xopt/generators/bayesian/visualize.py +0 -417
- {xopt-2.2.2 → xopt-2.3.0}/LICENSE +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/MANIFEST.in +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/README.md +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/benchmarks/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/benchmarks/test_mo.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/setup.cfg +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/setup.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_bax.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_bayesian_exploration.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_constraints.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_custom_model.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_hessian_kernel.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_high_level.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_mggpo.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_mobo.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_model_constructor.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_multi_fidelity.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_turbo.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_upper_confidence_bound.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/test_extremum_seeking.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/test_latin_hypercube.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/test_neldermead.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/test_random.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/generators/test_rcds.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/test_generator.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/test_io.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/test_mpi.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/test_numerical_optimizer.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/test_perf.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/test_pydantic.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/test_utils.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/tests/test_xopt.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/versioneer.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/asynchronous.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/base.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/errors.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/evaluator.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/base_model.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bax/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bax/acquisition.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bax_generator.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/constrained_acquisition.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/hessian_kernel.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/heteroskedastic.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/identity.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/log_acquisition_function.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/multi_fidelity.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/proximal.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/models/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/models/prior_mean.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/models/standard.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/models/time_dependent.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/multi_fidelity.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/time_dependent.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/turbo.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/es/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/es/extremumseeking.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/ga/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/ga/cnsga.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/ga/deap_creator.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/ga/deap_fitness_with_constraints.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/ga/fitness_with_constraints.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/random.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/rcds/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/rcds/rcds.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/scipy/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/scipy/latin_hypercube.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/utils.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/log.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/mpi/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/mpi/run.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/numerical_optimizer.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/pydantic.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/benchmarking.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/__init__.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/ackley_20.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/modified_tnk.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/multi_objective.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/problem.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/rosenbrock.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/sinusoid_1d.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/sphere_100.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/sphere_20.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/sphere_50.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/tnk.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/testing.py +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt.egg-info/dependency_links.txt +0 -0
- {xopt-2.2.2 → xopt-2.3.0}/xopt.egg-info/top_level.txt +0 -0
|
@@ -87,13 +87,13 @@ class TestBayesianGenerator(TestCase):
|
|
|
87
87
|
# try with input data that contains Nans due to xopt raising an error
|
|
88
88
|
# currently we drop all rows containing Nans
|
|
89
89
|
test_data = deepcopy(TEST_VOCS_DATA)
|
|
90
|
-
test_data["y1"].iloc[5] = np.
|
|
90
|
+
test_data["y1"].iloc[5] = np.nan
|
|
91
91
|
model = gen.train_model(test_data)
|
|
92
92
|
assert len(model.models[0].train_inputs[0]) == len(test_data) - 1
|
|
93
93
|
|
|
94
94
|
# test with input data that is only Nans
|
|
95
95
|
test_data = deepcopy(TEST_VOCS_DATA)
|
|
96
|
-
test_data["y1"].iloc[:] = np.
|
|
96
|
+
test_data["y1"].iloc[:] = np.nan
|
|
97
97
|
with pytest.raises(ValueError):
|
|
98
98
|
gen.train_model(test_data)
|
|
99
99
|
|
|
@@ -239,3 +239,12 @@ class TestBayesianGenerator(TestCase):
|
|
|
239
239
|
model.models[0].input_transform.bounds,
|
|
240
240
|
torch.tensor(((0, 0), (1, 10))).double(),
|
|
241
241
|
)
|
|
242
|
+
|
|
243
|
+
# test bad fixed feature name
|
|
244
|
+
gen = BayesianGenerator(vocs=TEST_VOCS_BASE)
|
|
245
|
+
gen.fixed_features = {"bad_name": 3.0}
|
|
246
|
+
data = deepcopy(TEST_VOCS_DATA)
|
|
247
|
+
gen.add_data(data)
|
|
248
|
+
|
|
249
|
+
with pytest.raises(KeyError):
|
|
250
|
+
gen.train_model()
|
|
@@ -7,6 +7,7 @@ from botorch.acquisition import ExpectedImprovement
|
|
|
7
7
|
from xopt.base import Xopt
|
|
8
8
|
from xopt.evaluator import Evaluator
|
|
9
9
|
from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator
|
|
10
|
+
from xopt.generators.bayesian.objectives import CustomXoptObjective
|
|
10
11
|
from xopt.resources.testing import TEST_VOCS_BASE, TEST_VOCS_DATA, xtest_callable
|
|
11
12
|
from xopt.vocs import ObjectiveEnum, VOCS
|
|
12
13
|
|
|
@@ -61,6 +62,28 @@ class TestExpectedImprovement:
|
|
|
61
62
|
for _ in range(1):
|
|
62
63
|
xopt.step()
|
|
63
64
|
|
|
65
|
+
def test_custom_objectives(self):
|
|
66
|
+
train_x = torch.tensor([0.01, 0.3, 0.6, 0.99]).double()
|
|
67
|
+
train_y = torch.sin(2 * torch.pi * train_x)
|
|
68
|
+
train_c = torch.cos(2 * torch.pi * train_x)
|
|
69
|
+
train_data = pd.DataFrame(
|
|
70
|
+
{"x1": train_x.numpy(), "y1": train_y.numpy(), "c1": train_c.numpy()}
|
|
71
|
+
)
|
|
72
|
+
vocs = VOCS(**{"variables": {"x1": [0.0, 1.0]}, "observables": ["y1", "c1"]})
|
|
73
|
+
|
|
74
|
+
class MyObjective(CustomXoptObjective):
|
|
75
|
+
def forward(self, samples, X=None):
|
|
76
|
+
return samples[..., self.vocs.output_names.index("y1")] ** 2
|
|
77
|
+
|
|
78
|
+
generator = ExpectedImprovementGenerator(
|
|
79
|
+
vocs=vocs, custom_objective=MyObjective(vocs)
|
|
80
|
+
)
|
|
81
|
+
generator.add_data(train_data)
|
|
82
|
+
best_f = generator._get_best_f(generator.data, generator.custom_objective)
|
|
83
|
+
assert float(best_f) == float(torch.max(train_y**2))
|
|
84
|
+
|
|
85
|
+
generator.generate(1)
|
|
86
|
+
|
|
64
87
|
def test_acquisition_accuracy(self):
|
|
65
88
|
train_x = torch.tensor([0.01, 0.3, 0.6, 0.99]).double()
|
|
66
89
|
train_y = torch.sin(2 * torch.pi * train_x)
|
|
@@ -10,7 +10,7 @@ class TestUtils:
|
|
|
10
10
|
def test_mobo_objective(self):
|
|
11
11
|
test_vocs_copy = deepcopy(TEST_VOCS_BASE)
|
|
12
12
|
test_vocs_copy.objectives["y2"] = "MAXIMIZE"
|
|
13
|
-
obj = create_mobo_objective(test_vocs_copy
|
|
13
|
+
obj = create_mobo_objective(test_vocs_copy)
|
|
14
14
|
|
|
15
15
|
# test large sample shape
|
|
16
16
|
test_samples = torch.randn(3, 4, 5, 3).double()
|
|
@@ -3,7 +3,8 @@ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import pandas as pd
|
|
5
5
|
|
|
6
|
-
from xopt import Evaluator
|
|
6
|
+
from xopt import Evaluator, Xopt
|
|
7
|
+
from xopt.generators import RandomGenerator
|
|
7
8
|
from xopt.vocs import VOCS
|
|
8
9
|
|
|
9
10
|
|
|
@@ -135,3 +136,29 @@ class TestEvaluator:
|
|
|
135
136
|
executor = ProcessPoolExecutor(max_workers=MAX_WORKERS)
|
|
136
137
|
ev = Evaluator(function=self.f, executor=executor, max_workers=MAX_WORKERS)
|
|
137
138
|
ev.evaluate_data(in10)
|
|
139
|
+
|
|
140
|
+
def test_evaluate_return_types(self):
|
|
141
|
+
def a(input):
|
|
142
|
+
return {"f": 1.0}
|
|
143
|
+
|
|
144
|
+
def b(input):
|
|
145
|
+
return {"f": [1.0]}
|
|
146
|
+
|
|
147
|
+
def c(input):
|
|
148
|
+
return {"f": np.array(1.0)}
|
|
149
|
+
|
|
150
|
+
def d(input):
|
|
151
|
+
return {"f": [1.0, 1.0]}
|
|
152
|
+
|
|
153
|
+
def e(input):
|
|
154
|
+
return {"f": np.ones(3)}
|
|
155
|
+
|
|
156
|
+
vocs = VOCS(variables={"x": [0, 1]}, objectives={"f": "MINIMIZE"})
|
|
157
|
+
|
|
158
|
+
for func in [a, b, c, d, e]:
|
|
159
|
+
evaluator = Evaluator(function=func)
|
|
160
|
+
generator = RandomGenerator(vocs=vocs)
|
|
161
|
+
|
|
162
|
+
X = Xopt(evaluator=evaluator, generator=generator, vocs=vocs)
|
|
163
|
+
|
|
164
|
+
X.random_evaluate(5)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import warnings
|
|
1
2
|
from copy import deepcopy
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
@@ -22,11 +23,25 @@ class TestVOCS(object):
|
|
|
22
23
|
assert vocs.n_outputs == 1
|
|
23
24
|
assert vocs.n_constraints == 0
|
|
24
25
|
|
|
26
|
+
def test_variable_validation(self):
|
|
27
|
+
with pytest.raises(ValidationError):
|
|
28
|
+
VOCS(
|
|
29
|
+
variables={"x": [1, 0]},
|
|
30
|
+
)
|
|
31
|
+
|
|
25
32
|
def test_empty_objectives(self):
|
|
26
33
|
VOCS(
|
|
27
34
|
variables={"x": [0, 1]},
|
|
28
35
|
)
|
|
29
36
|
|
|
37
|
+
def test_output_names(self):
|
|
38
|
+
test_vocs = VOCS(
|
|
39
|
+
variables={"x": [0, 1]},
|
|
40
|
+
objectives={"y1": "MINIMIZE"},
|
|
41
|
+
constraints={"c1": ["GREATER_THAN", 0], "c2": ["LESS_THAN", 0]},
|
|
42
|
+
)
|
|
43
|
+
assert test_vocs.output_names == ["y1", "c1", "c2"]
|
|
44
|
+
|
|
30
45
|
def test_constraint_specification(self):
|
|
31
46
|
good_constraint_list = [
|
|
32
47
|
["LESS_THAN", 0],
|
|
@@ -117,18 +132,39 @@ class TestVOCS(object):
|
|
|
117
132
|
assert vocs.n_outputs == 2
|
|
118
133
|
assert vocs.variable_names == ["x1"]
|
|
119
134
|
|
|
120
|
-
def
|
|
135
|
+
def test_random_sampling_custom_bounds(self):
|
|
121
136
|
vocs = deepcopy(TEST_VOCS_BASE)
|
|
122
137
|
|
|
123
138
|
custom_bounds = {"x1": [0.5, 0.75], "x2": [7.5, 15.0]}
|
|
124
139
|
|
|
125
|
-
|
|
140
|
+
with pytest.warns(RuntimeWarning):
|
|
141
|
+
random_input_data = vocs.random_inputs(100, custom_bounds=custom_bounds)
|
|
142
|
+
|
|
126
143
|
random_input_data = pd.DataFrame(random_input_data)
|
|
127
144
|
assert all(random_input_data["x1"] < 0.75)
|
|
128
145
|
assert all(random_input_data["x1"] > 0.5)
|
|
129
146
|
assert all(random_input_data["x2"] > 7.5)
|
|
130
147
|
assert all(random_input_data["x2"] < 10.0)
|
|
131
148
|
|
|
149
|
+
# test custom bounds within the vocs domain -- no warnings should be raised
|
|
150
|
+
in_domain_custom_bounds = {"x1": [0.5, 0.75], "x2": [0.5, 0.75]}
|
|
151
|
+
with warnings.catch_warnings():
|
|
152
|
+
warnings.simplefilter("ignore")
|
|
153
|
+
vocs.random_inputs(100, custom_bounds=in_domain_custom_bounds)
|
|
154
|
+
|
|
155
|
+
# test wrong type
|
|
156
|
+
with pytest.raises(TypeError):
|
|
157
|
+
vocs.random_inputs(100, custom_bounds=1)
|
|
158
|
+
|
|
159
|
+
# test custom bounds entirely outside the vocs domain or specified incorrectly
|
|
160
|
+
bad_custom_bounds = [
|
|
161
|
+
{"x1": [10.0, 10.75], "x2": [7.5, 15.0]},
|
|
162
|
+
{"x1": [0.75, 0.5], "x2": [7.5, 15.0]},
|
|
163
|
+
]
|
|
164
|
+
for ele in bad_custom_bounds:
|
|
165
|
+
with pytest.raises(ValueError):
|
|
166
|
+
vocs.random_inputs(100, custom_bounds=ele)
|
|
167
|
+
|
|
132
168
|
def test_duplicate_outputs(self):
|
|
133
169
|
vocs = deepcopy(TEST_VOCS_BASE)
|
|
134
170
|
assert vocs.output_names == ["y1", "c1"]
|
|
@@ -234,6 +270,14 @@ class TestVOCS(object):
|
|
|
234
270
|
assert idx == 1
|
|
235
271
|
assert val == 0.1
|
|
236
272
|
|
|
273
|
+
# test error handling
|
|
274
|
+
with pytest.raises(RuntimeError):
|
|
275
|
+
vocs.select_best(pd.DataFrame())
|
|
276
|
+
|
|
277
|
+
vocs.constraints = {"c1": ["GREATER_THAN", 10.5]}
|
|
278
|
+
with pytest.raises(RuntimeError):
|
|
279
|
+
vocs.select_best(pd.DataFrame())
|
|
280
|
+
|
|
237
281
|
@pytest.mark.filterwarnings("ignore: All-NaN axis encountered")
|
|
238
282
|
def test_cumulative_optimum(self):
|
|
239
283
|
vocs = deepcopy(TEST_VOCS_BASE)
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2024-
|
|
11
|
+
"date": "2024-06-18T12:58:34-0500",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "2.
|
|
14
|
+
"full-revisionid": "b83cda7ec8623f60a26a37a522e8933ed2895b84",
|
|
15
|
+
"version": "2.3.0"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -42,7 +42,7 @@ class Generator(XoptBaseModel, ABC):
|
|
|
42
42
|
|
|
43
43
|
@field_validator("vocs", mode="after")
|
|
44
44
|
def validate_vocs(cls, v, info: ValidationInfo):
|
|
45
|
-
if v.n_objectives
|
|
45
|
+
if v.n_objectives > 1 and not info.data["supports_multi_objective"]:
|
|
46
46
|
raise ValueError("this generator only supports single objective")
|
|
47
47
|
return v
|
|
48
48
|
|
|
@@ -76,35 +76,31 @@ def get_generator_dynamic(name: str):
|
|
|
76
76
|
"WARNING: `scipy` not found, NelderMeadGenerator and LatinHypercubeGenerator are not available"
|
|
77
77
|
)
|
|
78
78
|
elif name in all_generator_names["bo"]:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
from xopt.generators.bayesian.bayesian_exploration import (
|
|
80
|
+
BayesianExplorationGenerator,
|
|
81
|
+
)
|
|
82
|
+
from xopt.generators.bayesian.expected_improvement import (
|
|
83
|
+
ExpectedImprovementGenerator,
|
|
84
|
+
)
|
|
85
|
+
from xopt.generators.bayesian.mobo import MOBOGenerator
|
|
86
|
+
from xopt.generators.bayesian.multi_fidelity import MultiFidelityGenerator
|
|
87
|
+
from xopt.generators.bayesian.upper_confidence_bound import (
|
|
88
|
+
TDUpperConfidenceBoundGenerator,
|
|
89
|
+
UpperConfidenceBoundGenerator,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
registered_generators = [
|
|
93
|
+
UpperConfidenceBoundGenerator,
|
|
94
|
+
MOBOGenerator,
|
|
95
|
+
BayesianExplorationGenerator,
|
|
96
|
+
TDUpperConfidenceBoundGenerator,
|
|
97
|
+
ExpectedImprovementGenerator,
|
|
98
|
+
MultiFidelityGenerator,
|
|
99
|
+
]
|
|
100
|
+
for gen in registered_generators:
|
|
101
|
+
generators[gen.name] = gen
|
|
102
|
+
return generators[name]
|
|
92
103
|
|
|
93
|
-
registered_generators = [
|
|
94
|
-
UpperConfidenceBoundGenerator,
|
|
95
|
-
MOBOGenerator,
|
|
96
|
-
BayesianExplorationGenerator,
|
|
97
|
-
TDUpperConfidenceBoundGenerator,
|
|
98
|
-
ExpectedImprovementGenerator,
|
|
99
|
-
MultiFidelityGenerator,
|
|
100
|
-
]
|
|
101
|
-
for gen in registered_generators:
|
|
102
|
-
generators[gen.name] = gen
|
|
103
|
-
return generators[name]
|
|
104
|
-
except ModuleNotFoundError:
|
|
105
|
-
warnings.warn(
|
|
106
|
-
"WARNING: `botorch` not found, Bayesian generators are not available"
|
|
107
|
-
)
|
|
108
104
|
elif name in all_generator_names["ga"]:
|
|
109
105
|
try:
|
|
110
106
|
from xopt.generators.ga import CNSGAGenerator
|
|
@@ -21,6 +21,24 @@ class Algorithm(XoptBaseModel, ABC):
|
|
|
21
21
|
) -> Tuple[Tensor, Tensor, Dict]:
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def evaluate_virtual_objective(
|
|
26
|
+
self,
|
|
27
|
+
model: Model,
|
|
28
|
+
x: Tensor,
|
|
29
|
+
bounds: Tensor,
|
|
30
|
+
n_samples: int,
|
|
31
|
+
tkwargs: dict = None,
|
|
32
|
+
) -> Tensor:
|
|
33
|
+
"""
|
|
34
|
+
Evaluates virtual objective at inputs given by x.
|
|
35
|
+
Inputs:
|
|
36
|
+
x: tensor, shape `num_points x ndim`
|
|
37
|
+
Returns:
|
|
38
|
+
objective_values: tensor, shape `n_samples x num_points x 1`
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
24
42
|
|
|
25
43
|
class GridScanAlgorithm(Algorithm, ABC):
|
|
26
44
|
name = "grid_scan_algorithm"
|
|
@@ -60,21 +78,40 @@ class GridMinimize(GridScanAlgorithm):
|
|
|
60
78
|
test_points = self.create_mesh(bounds).to(model.models[0].train_targets)
|
|
61
79
|
|
|
62
80
|
# get samples of the model posterior at mesh points
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
posterior_samples = self.evaluate_virtual_objective(
|
|
82
|
+
model, test_points, bounds, self.n_samples
|
|
83
|
+
)
|
|
66
84
|
|
|
67
85
|
# get points that minimize each sample (execution paths)
|
|
68
|
-
y_min, min_idx = torch.min(
|
|
86
|
+
y_min, min_idx = torch.min(posterior_samples, dim=-2)
|
|
69
87
|
min_idx = min_idx.squeeze()
|
|
70
88
|
x_min = test_points[min_idx]
|
|
71
89
|
|
|
72
90
|
# collect secondary results in a dict
|
|
73
91
|
results_dict = {
|
|
74
92
|
"test_points": test_points,
|
|
75
|
-
"posterior_samples":
|
|
93
|
+
"posterior_samples": posterior_samples,
|
|
76
94
|
"execution_paths": torch.hstack((x_min, y_min)),
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
# return execution paths
|
|
80
98
|
return x_min.unsqueeze(-2), y_min.unsqueeze(-2), results_dict
|
|
99
|
+
|
|
100
|
+
def evaluate_virtual_objective(
|
|
101
|
+
self,
|
|
102
|
+
model: Model,
|
|
103
|
+
x: Tensor,
|
|
104
|
+
bounds: Tensor,
|
|
105
|
+
n_samples: int,
|
|
106
|
+
tkwargs: dict = None,
|
|
107
|
+
) -> Tensor:
|
|
108
|
+
"""Evaluate virtual objective (samples)"""
|
|
109
|
+
|
|
110
|
+
tkwargs = tkwargs if tkwargs else {"dtype": torch.double, "device": "cpu"}
|
|
111
|
+
|
|
112
|
+
# get samples of the model posterior at inputs given by x
|
|
113
|
+
with torch.no_grad():
|
|
114
|
+
post = model.posterior(x)
|
|
115
|
+
objective_values = post.rsample(torch.Size([n_samples]))
|
|
116
|
+
|
|
117
|
+
return objective_values
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from matplotlib import pyplot as plt
|
|
3
|
+
|
|
4
|
+
from xopt.generators.bayesian.visualize import (
|
|
5
|
+
_generate_input_mesh,
|
|
6
|
+
_get_reference_point,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def visualize_virtual_objective(
|
|
11
|
+
generator,
|
|
12
|
+
variable_names: list[str] = None,
|
|
13
|
+
idx: int = -1,
|
|
14
|
+
reference_point: dict = None,
|
|
15
|
+
show_samples: bool = True,
|
|
16
|
+
n_grid: int = 50,
|
|
17
|
+
n_samples: int = 100,
|
|
18
|
+
kwargs: dict = None,
|
|
19
|
+
) -> tuple:
|
|
20
|
+
"""
|
|
21
|
+
Displays BAX's virtual objective predictions computed from samples drawn
|
|
22
|
+
from the GP model(s) of the observable(s).
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
generator : Generator
|
|
27
|
+
Bayesian generator object.
|
|
28
|
+
variable_names : List[str]
|
|
29
|
+
The variables with respect to which the GP models are displayed (maximum
|
|
30
|
+
of 2). Defaults to generator.vocs.variable_names.
|
|
31
|
+
idx : int
|
|
32
|
+
Index of the last sample to use. This also selects the point of reference in
|
|
33
|
+
higher dimensions unless an explicit reference_point is given.
|
|
34
|
+
reference_point : dict
|
|
35
|
+
Reference point determining the value of variables in
|
|
36
|
+
generator.vocs.variable_names, but not in variable_names (slice plots in
|
|
37
|
+
higher dimensions). Defaults to last used sample.
|
|
38
|
+
show_samples : bool, optional
|
|
39
|
+
Whether samples are shown.
|
|
40
|
+
n_grid : int, optional
|
|
41
|
+
Number of grid points per dimension used to display the model predictions.
|
|
42
|
+
n_samples : int, optional
|
|
43
|
+
Number of virtual objective samples to evaluate for each point in the scan.
|
|
44
|
+
kwargs : dict, optional
|
|
45
|
+
Additional keyword arguments for evaluating the virtual objective.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
--------
|
|
49
|
+
The matplotlib figure and axes objects.
|
|
50
|
+
"""
|
|
51
|
+
vocs, data = generator.vocs, generator.data
|
|
52
|
+
reference_point = _get_reference_point(reference_point, vocs, data, idx)
|
|
53
|
+
# define output and variable names
|
|
54
|
+
if variable_names is None:
|
|
55
|
+
variable_names = vocs.variable_names
|
|
56
|
+
dim_x = len(variable_names)
|
|
57
|
+
if dim_x not in [1, 2]:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Visualization is only supported with respect to 1 or 2 variables, "
|
|
60
|
+
f"not {dim_x}. Provide a compatible list of variable names to create "
|
|
61
|
+
f"slice plots at higher dimensions."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# validate variable names
|
|
65
|
+
invalid = [name not in getattr(vocs, "variable_names") for name in variable_names]
|
|
66
|
+
if any(invalid):
|
|
67
|
+
invalid_names = [
|
|
68
|
+
variable_names[i] for i in range(len(variable_names)) if invalid[i]
|
|
69
|
+
]
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Variable names {invalid_names} are not in generator.vocs.variable_names."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# validate reference point keys
|
|
75
|
+
invalid = [
|
|
76
|
+
name not in getattr(vocs, "variable_names") for name in [*reference_point]
|
|
77
|
+
]
|
|
78
|
+
if any(invalid):
|
|
79
|
+
invalid_names = [
|
|
80
|
+
[*reference_point][i] for i in range(len([*reference_point])) if invalid[i]
|
|
81
|
+
]
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"reference_point contains keys {invalid_names}, "
|
|
84
|
+
f"which are not in generator.vocs.variable_names."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
x = _generate_input_mesh(vocs, variable_names, reference_point, n_grid)
|
|
88
|
+
|
|
89
|
+
# verify model exists
|
|
90
|
+
if generator.model is None:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"The generator.model doesn't exist, try calling generator.train_model()."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# subset bax observable models
|
|
96
|
+
bax_model_ids = [
|
|
97
|
+
generator.vocs.output_names.index(name)
|
|
98
|
+
for name in generator.algorithm.observable_names_ordered
|
|
99
|
+
]
|
|
100
|
+
bax_model = generator.model.subset_output(bax_model_ids)
|
|
101
|
+
|
|
102
|
+
# get virtual objective (sample) values
|
|
103
|
+
bounds = generator._get_optimization_bounds()
|
|
104
|
+
kwargs = kwargs if kwargs else {}
|
|
105
|
+
objective_values = generator.algorithm.evaluate_virtual_objective(
|
|
106
|
+
bax_model, x, bounds, tkwargs=generator._tkwargs, n_samples=n_samples, **kwargs
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# get sample stats
|
|
110
|
+
objective_med = objective_values.nanmedian(dim=0)[0].flatten()
|
|
111
|
+
objective_upper = torch.nanquantile(objective_values, q=0.975, dim=0).flatten()
|
|
112
|
+
objective_lower = torch.nanquantile(objective_values, q=0.025, dim=0).flatten()
|
|
113
|
+
objective_std = (objective_upper - objective_lower) / 4
|
|
114
|
+
|
|
115
|
+
figsize = (4 * dim_x, 3.7)
|
|
116
|
+
fig, ax = plt.subplots(
|
|
117
|
+
nrows=1, ncols=dim_x, sharex=True, sharey=True, figsize=figsize
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if dim_x == 1:
|
|
121
|
+
# 1d line plot
|
|
122
|
+
x_axis = x[:, vocs.variable_names.index(variable_names[0])].squeeze().numpy()
|
|
123
|
+
ax.plot(x_axis, objective_med, color="C0", label="Median")
|
|
124
|
+
ax.fill_between(
|
|
125
|
+
x_axis,
|
|
126
|
+
objective_lower,
|
|
127
|
+
objective_upper,
|
|
128
|
+
color="C0",
|
|
129
|
+
alpha=0.5,
|
|
130
|
+
label="95% C.I.",
|
|
131
|
+
)
|
|
132
|
+
ax.legend()
|
|
133
|
+
ax.set_ylabel("Virtual Objective")
|
|
134
|
+
ax.set_xlabel(variable_names[0])
|
|
135
|
+
else:
|
|
136
|
+
# 2d heatmaps
|
|
137
|
+
for j in [0, 1]:
|
|
138
|
+
ax_j = ax[j]
|
|
139
|
+
ax_j.locator_params(axis="both", nbins=5)
|
|
140
|
+
if j == 0:
|
|
141
|
+
prediction = objective_med
|
|
142
|
+
title = "Objective Median"
|
|
143
|
+
cbar_label = "Objective Median"
|
|
144
|
+
elif j == 1:
|
|
145
|
+
prediction = objective_std
|
|
146
|
+
title = "Objective SD"
|
|
147
|
+
cbar_label = r"$\sigma\,$[Objective]"
|
|
148
|
+
|
|
149
|
+
pcm = ax_j.pcolormesh(
|
|
150
|
+
x[:, vocs.variable_names.index(variable_names[0])]
|
|
151
|
+
.reshape(n_grid, n_grid)
|
|
152
|
+
.numpy(),
|
|
153
|
+
x[:, vocs.variable_names.index(variable_names[1])]
|
|
154
|
+
.reshape(n_grid, n_grid)
|
|
155
|
+
.numpy(),
|
|
156
|
+
prediction.reshape(n_grid, n_grid),
|
|
157
|
+
rasterized=True,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable # lazy import
|
|
161
|
+
|
|
162
|
+
divider = make_axes_locatable(ax_j)
|
|
163
|
+
cax = divider.append_axes("right", size="5%", pad=0.1)
|
|
164
|
+
cbar = plt.colorbar(pcm, cax=cax)
|
|
165
|
+
ax_j.set_title(title)
|
|
166
|
+
ax_j.set_xlabel(variable_names[0])
|
|
167
|
+
ax_j.set_ylabel(variable_names[1])
|
|
168
|
+
cbar.set_label(cbar_label)
|
|
169
|
+
|
|
170
|
+
fig.tight_layout()
|
|
171
|
+
return fig, ax
|
|
@@ -13,7 +13,6 @@ from xopt.generators.bayesian.bayesian_generator import (
|
|
|
13
13
|
BayesianGenerator,
|
|
14
14
|
formatted_base_docstring,
|
|
15
15
|
)
|
|
16
|
-
from xopt.generators.bayesian.objectives import create_exploration_objective
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
class BayesianExplorationGenerator(BayesianGenerator):
|
|
@@ -38,12 +37,6 @@ class BayesianExplorationGenerator(BayesianGenerator):
|
|
|
38
37
|
|
|
39
38
|
return qPV
|
|
40
39
|
|
|
41
|
-
def _get_objective(self):
|
|
42
|
-
"""return exploration objective, which only captures the output of the first
|
|
43
|
-
model output"""
|
|
44
|
-
|
|
45
|
-
return create_exploration_objective(self.vocs, self._tkwargs)
|
|
46
|
-
|
|
47
40
|
|
|
48
41
|
class qPosteriorVariance(MCAcquisitionFunction):
|
|
49
42
|
def __init__(
|
|
@@ -30,6 +30,7 @@ from xopt.generators.bayesian.models.standard import StandardModelConstructor
|
|
|
30
30
|
from xopt.generators.bayesian.objectives import (
|
|
31
31
|
create_constraint_callables,
|
|
32
32
|
create_mc_objective,
|
|
33
|
+
CustomXoptObjective,
|
|
33
34
|
)
|
|
34
35
|
from xopt.generators.bayesian.turbo import (
|
|
35
36
|
OptimizeTurboController,
|
|
@@ -160,6 +161,10 @@ class BayesianGenerator(Generator, ABC):
|
|
|
160
161
|
False,
|
|
161
162
|
description="flag to log transform the acquisition function before optimization",
|
|
162
163
|
)
|
|
164
|
+
custom_objective: Optional[CustomXoptObjective] = Field(
|
|
165
|
+
None,
|
|
166
|
+
description="custom objective for optimization, replaces objective specified by VOCS",
|
|
167
|
+
)
|
|
163
168
|
n_interpolate_points: Optional[PositiveInt] = None
|
|
164
169
|
|
|
165
170
|
n_candidates: int = 1
|
|
@@ -375,6 +380,11 @@ class BayesianGenerator(Generator, ABC):
|
|
|
375
380
|
# get bounds for each fixed_feature (vocs bounds take precedent)
|
|
376
381
|
for key in self.fixed_features:
|
|
377
382
|
if key not in variable_bounds:
|
|
383
|
+
if key not in data:
|
|
384
|
+
raise KeyError(
|
|
385
|
+
"generator data needs to contain fixed feature "
|
|
386
|
+
f"column name `{key}`"
|
|
387
|
+
)
|
|
378
388
|
f_data = data[key]
|
|
379
389
|
bounds = [f_data.min(), f_data.max()]
|
|
380
390
|
if bounds[1] - bounds[0] < 1e-8:
|
|
@@ -545,11 +555,24 @@ class BayesianGenerator(Generator, ABC):
|
|
|
545
555
|
pass
|
|
546
556
|
|
|
547
557
|
def _get_objective(self):
|
|
548
|
-
"""return default objective (scalar objective) determined by vocs
|
|
549
|
-
|
|
558
|
+
"""return default objective (scalar objective) determined by vocs or if
|
|
559
|
+
defined in custom_objective"""
|
|
560
|
+
# check to make sure that if we specify a custom objective that no objectives
|
|
561
|
+
# are specified in vocs
|
|
562
|
+
if self.custom_objective is not None:
|
|
563
|
+
if self.vocs.n_objectives:
|
|
564
|
+
raise RuntimeError(
|
|
565
|
+
"cannot specify objectives in VOCS "
|
|
566
|
+
"and a custom objective for the generator at the "
|
|
567
|
+
"same time"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
return self.custom_objective
|
|
571
|
+
else:
|
|
572
|
+
return create_mc_objective(self.vocs, self._tkwargs)
|
|
550
573
|
|
|
551
574
|
def _get_constraint_callables(self):
|
|
552
|
-
"""return
|
|
575
|
+
"""return constratint callable determined by vocs"""
|
|
553
576
|
constraint_callables = create_constraint_callables(self.vocs)
|
|
554
577
|
if len(constraint_callables) == 0:
|
|
555
578
|
constraint_callables = None
|
|
@@ -755,7 +778,7 @@ class MultiObjectiveBayesianGenerator(BayesianGenerator, ABC):
|
|
|
755
778
|
|
|
756
779
|
n_objectives = self.vocs.n_objectives
|
|
757
780
|
weights = torch.zeros(n_objectives)
|
|
758
|
-
weights = set_botorch_weights(
|
|
781
|
+
weights = set_botorch_weights(self.vocs).to(**self._tkwargs)
|
|
759
782
|
objective_data = objective_data * weights
|
|
760
783
|
|
|
761
784
|
# compute hypervolume
|