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.
Files changed (125) hide show
  1. {xopt-2.2.2/xopt.egg-info → xopt-2.3.0}/PKG-INFO +1 -1
  2. {xopt-2.2.2 → xopt-2.3.0}/requirements.txt +1 -1
  3. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_bayesian_generator.py +11 -2
  4. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_expected_improvement.py +23 -0
  5. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_time_dependent_bo.py +1 -1
  6. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_utils.py +1 -1
  7. {xopt-2.2.2 → xopt-2.3.0}/tests/test_evaluator.py +28 -1
  8. {xopt-2.2.2 → xopt-2.3.0}/tests/test_vocs.py +46 -2
  9. {xopt-2.2.2 → xopt-2.3.0}/xopt/_version.py +3 -3
  10. {xopt-2.2.2 → xopt-2.3.0}/xopt/generator.py +1 -1
  11. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/__init__.py +24 -28
  12. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bax/algorithms.py +42 -5
  13. xopt-2.3.0/xopt/generators/bayesian/bax/visualize.py +171 -0
  14. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bayesian_exploration.py +0 -7
  15. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bayesian_generator.py +27 -4
  16. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/expected_improvement.py +25 -7
  17. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/mggpo.py +1 -1
  18. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/mobo.py +1 -1
  19. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/objectives.py +47 -22
  20. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/upper_confidence_bound.py +4 -4
  21. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/utils.py +20 -7
  22. xopt-2.3.0/xopt/generators/bayesian/visualize.py +1095 -0
  23. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/scipy/neldermead.py +2 -3
  24. xopt-2.3.0/xopt/resources/test_functions/haverly_pooling.py +59 -0
  25. {xopt-2.2.2 → xopt-2.3.0}/xopt/utils.py +5 -1
  26. {xopt-2.2.2 → xopt-2.3.0}/xopt/vocs.py +60 -2
  27. {xopt-2.2.2 → xopt-2.3.0/xopt.egg-info}/PKG-INFO +1 -1
  28. {xopt-2.2.2 → xopt-2.3.0}/xopt.egg-info/SOURCES.txt +2 -0
  29. {xopt-2.2.2 → xopt-2.3.0}/xopt.egg-info/requires.txt +1 -1
  30. xopt-2.2.2/xopt/generators/bayesian/visualize.py +0 -417
  31. {xopt-2.2.2 → xopt-2.3.0}/LICENSE +0 -0
  32. {xopt-2.2.2 → xopt-2.3.0}/MANIFEST.in +0 -0
  33. {xopt-2.2.2 → xopt-2.3.0}/README.md +0 -0
  34. {xopt-2.2.2 → xopt-2.3.0}/benchmarks/__init__.py +0 -0
  35. {xopt-2.2.2 → xopt-2.3.0}/benchmarks/test_mo.py +0 -0
  36. {xopt-2.2.2 → xopt-2.3.0}/setup.cfg +0 -0
  37. {xopt-2.2.2 → xopt-2.3.0}/setup.py +0 -0
  38. {xopt-2.2.2 → xopt-2.3.0}/tests/__init__.py +0 -0
  39. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/__init__.py +0 -0
  40. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/__init__.py +0 -0
  41. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_bax.py +0 -0
  42. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_bayesian_exploration.py +0 -0
  43. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_constraints.py +0 -0
  44. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_custom_model.py +0 -0
  45. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_hessian_kernel.py +0 -0
  46. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_high_level.py +0 -0
  47. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_mggpo.py +0 -0
  48. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_mobo.py +0 -0
  49. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_model_constructor.py +0 -0
  50. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_multi_fidelity.py +0 -0
  51. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_turbo.py +0 -0
  52. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/bayesian/test_upper_confidence_bound.py +0 -0
  53. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/test_extremum_seeking.py +0 -0
  54. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/test_latin_hypercube.py +0 -0
  55. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/test_neldermead.py +0 -0
  56. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/test_random.py +0 -0
  57. {xopt-2.2.2 → xopt-2.3.0}/tests/generators/test_rcds.py +0 -0
  58. {xopt-2.2.2 → xopt-2.3.0}/tests/test_generator.py +0 -0
  59. {xopt-2.2.2 → xopt-2.3.0}/tests/test_io.py +0 -0
  60. {xopt-2.2.2 → xopt-2.3.0}/tests/test_mpi.py +0 -0
  61. {xopt-2.2.2 → xopt-2.3.0}/tests/test_numerical_optimizer.py +0 -0
  62. {xopt-2.2.2 → xopt-2.3.0}/tests/test_perf.py +0 -0
  63. {xopt-2.2.2 → xopt-2.3.0}/tests/test_pydantic.py +0 -0
  64. {xopt-2.2.2 → xopt-2.3.0}/tests/test_utils.py +0 -0
  65. {xopt-2.2.2 → xopt-2.3.0}/tests/test_xopt.py +0 -0
  66. {xopt-2.2.2 → xopt-2.3.0}/versioneer.py +0 -0
  67. {xopt-2.2.2 → xopt-2.3.0}/xopt/__init__.py +0 -0
  68. {xopt-2.2.2 → xopt-2.3.0}/xopt/asynchronous.py +0 -0
  69. {xopt-2.2.2 → xopt-2.3.0}/xopt/base.py +0 -0
  70. {xopt-2.2.2 → xopt-2.3.0}/xopt/errors.py +0 -0
  71. {xopt-2.2.2 → xopt-2.3.0}/xopt/evaluator.py +0 -0
  72. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/__init__.py +0 -0
  73. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/base_model.py +0 -0
  74. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bax/__init__.py +0 -0
  75. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bax/acquisition.py +0 -0
  76. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/bax_generator.py +0 -0
  77. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/__init__.py +0 -0
  78. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/constrained_acquisition.py +0 -0
  79. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/hessian_kernel.py +0 -0
  80. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/heteroskedastic.py +0 -0
  81. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/identity.py +0 -0
  82. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/log_acquisition_function.py +0 -0
  83. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/multi_fidelity.py +0 -0
  84. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/proximal.py +0 -0
  85. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/models/__init__.py +0 -0
  86. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/models/prior_mean.py +0 -0
  87. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/models/standard.py +0 -0
  88. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/models/time_dependent.py +0 -0
  89. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/multi_fidelity.py +0 -0
  90. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/time_dependent.py +0 -0
  91. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/bayesian/turbo.py +0 -0
  92. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/es/__init__.py +0 -0
  93. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/es/extremumseeking.py +0 -0
  94. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/ga/__init__.py +0 -0
  95. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/ga/cnsga.py +0 -0
  96. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/ga/deap_creator.py +0 -0
  97. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/ga/deap_fitness_with_constraints.py +0 -0
  98. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/ga/fitness_with_constraints.py +0 -0
  99. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/random.py +0 -0
  100. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/rcds/__init__.py +0 -0
  101. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/rcds/rcds.py +0 -0
  102. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/scipy/__init__.py +0 -0
  103. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/scipy/latin_hypercube.py +0 -0
  104. {xopt-2.2.2 → xopt-2.3.0}/xopt/generators/utils.py +0 -0
  105. {xopt-2.2.2 → xopt-2.3.0}/xopt/log.py +0 -0
  106. {xopt-2.2.2 → xopt-2.3.0}/xopt/mpi/__init__.py +0 -0
  107. {xopt-2.2.2 → xopt-2.3.0}/xopt/mpi/run.py +0 -0
  108. {xopt-2.2.2 → xopt-2.3.0}/xopt/numerical_optimizer.py +0 -0
  109. {xopt-2.2.2 → xopt-2.3.0}/xopt/pydantic.py +0 -0
  110. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/__init__.py +0 -0
  111. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/benchmarking.py +0 -0
  112. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/__init__.py +0 -0
  113. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/ackley_20.py +0 -0
  114. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/modified_tnk.py +0 -0
  115. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/multi_objective.py +0 -0
  116. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/problem.py +0 -0
  117. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/rosenbrock.py +0 -0
  118. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/sinusoid_1d.py +0 -0
  119. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/sphere_100.py +0 -0
  120. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/sphere_20.py +0 -0
  121. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/sphere_50.py +0 -0
  122. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/test_functions/tnk.py +0 -0
  123. {xopt-2.2.2 → xopt-2.3.0}/xopt/resources/testing.py +0 -0
  124. {xopt-2.2.2 → xopt-2.3.0}/xopt.egg-info/dependency_links.txt +0 -0
  125. {xopt-2.2.2 → xopt-2.3.0}/xopt.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: xopt
3
- Version: 2.2.2
3
+ Version: 2.3.0
4
4
  Home-page: https://github.com/ChristopherMayes/xopt
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -2,7 +2,7 @@ deap
2
2
  numpy
3
3
  pyyaml
4
4
  torch
5
- botorch>=0.9.2
5
+ botorch>=0.9.2,<=0.10.0
6
6
  gpytorch
7
7
  pandas
8
8
  pydantic>=2.3
@@ -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.NaN
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.NaN
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)
@@ -32,7 +32,7 @@ class TestTimeDependentBO:
32
32
  model = gen.train_model(test_data)
33
33
 
34
34
  # make sure time data is in the last model
35
- assert np.alltrue(
35
+ assert np.all(
36
36
  model.models[-1]
37
37
  .input_transform.untransform(model.models[-1].train_inputs[0])[:, -1]
38
38
  .numpy()
@@ -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, tkwargs={})
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 test_custom_bounds(self):
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
- random_input_data = vocs.random_inputs(100, custom_bounds=custom_bounds)
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-04-15T14:51:35-0400",
11
+ "date": "2024-06-18T12:58:34-0500",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "a613b00abdb1ff1809a10b82096947803834cec5",
15
- "version": "2.2.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 != 1 and not info.data["supports_multi_objective"]:
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
- try:
80
- from xopt.generators.bayesian.bayesian_exploration import (
81
- BayesianExplorationGenerator,
82
- )
83
- from xopt.generators.bayesian.expected_improvement import (
84
- ExpectedImprovementGenerator,
85
- )
86
- from xopt.generators.bayesian.mobo import MOBOGenerator
87
- from xopt.generators.bayesian.multi_fidelity import MultiFidelityGenerator
88
- from xopt.generators.bayesian.upper_confidence_bound import (
89
- TDUpperConfidenceBoundGenerator,
90
- UpperConfidenceBoundGenerator,
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
- with torch.no_grad():
64
- post = model.posterior(test_points)
65
- post_samples = post.rsample(torch.Size([self.n_samples]))
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(post_samples, dim=-2)
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": post_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
- return create_mc_objective(self.vocs, self._tkwargs)
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 default objective (scalar objective) determined by vocs"""
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(weights, self.vocs)
781
+ weights = set_botorch_weights(self.vocs).to(**self._tkwargs)
759
782
  objective_data = objective_data * weights
760
783
 
761
784
  # compute hypervolume