xopt 2.2.3__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 (124) hide show
  1. {xopt-2.2.3/xopt.egg-info → xopt-2.3.0}/PKG-INFO +1 -1
  2. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_bayesian_generator.py +2 -2
  3. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_expected_improvement.py +23 -0
  4. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_time_dependent_bo.py +1 -1
  5. {xopt-2.2.3 → xopt-2.3.0}/tests/test_vocs.py +30 -2
  6. {xopt-2.2.3 → xopt-2.3.0}/xopt/_version.py +3 -3
  7. {xopt-2.2.3 → xopt-2.3.0}/xopt/generator.py +1 -1
  8. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/__init__.py +24 -28
  9. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/bax/algorithms.py +42 -5
  10. xopt-2.3.0/xopt/generators/bayesian/bax/visualize.py +171 -0
  11. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/bayesian_generator.py +19 -10
  12. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/expected_improvement.py +24 -5
  13. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/objectives.py +40 -1
  14. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/upper_confidence_bound.py +3 -1
  15. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/visualize.py +6 -2
  16. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/scipy/neldermead.py +2 -3
  17. xopt-2.3.0/xopt/resources/test_functions/haverly_pooling.py +59 -0
  18. {xopt-2.2.3 → xopt-2.3.0}/xopt/vocs.py +52 -2
  19. {xopt-2.2.3 → xopt-2.3.0/xopt.egg-info}/PKG-INFO +1 -1
  20. {xopt-2.2.3 → xopt-2.3.0}/xopt.egg-info/SOURCES.txt +2 -0
  21. {xopt-2.2.3 → xopt-2.3.0}/LICENSE +0 -0
  22. {xopt-2.2.3 → xopt-2.3.0}/MANIFEST.in +0 -0
  23. {xopt-2.2.3 → xopt-2.3.0}/README.md +0 -0
  24. {xopt-2.2.3 → xopt-2.3.0}/benchmarks/__init__.py +0 -0
  25. {xopt-2.2.3 → xopt-2.3.0}/benchmarks/test_mo.py +0 -0
  26. {xopt-2.2.3 → xopt-2.3.0}/requirements.txt +0 -0
  27. {xopt-2.2.3 → xopt-2.3.0}/setup.cfg +0 -0
  28. {xopt-2.2.3 → xopt-2.3.0}/setup.py +0 -0
  29. {xopt-2.2.3 → xopt-2.3.0}/tests/__init__.py +0 -0
  30. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/__init__.py +0 -0
  31. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/__init__.py +0 -0
  32. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_bax.py +0 -0
  33. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_bayesian_exploration.py +0 -0
  34. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_constraints.py +0 -0
  35. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_custom_model.py +0 -0
  36. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_hessian_kernel.py +0 -0
  37. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_high_level.py +0 -0
  38. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_mggpo.py +0 -0
  39. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_mobo.py +0 -0
  40. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_model_constructor.py +0 -0
  41. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_multi_fidelity.py +0 -0
  42. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_turbo.py +0 -0
  43. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_upper_confidence_bound.py +0 -0
  44. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/bayesian/test_utils.py +0 -0
  45. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/test_extremum_seeking.py +0 -0
  46. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/test_latin_hypercube.py +0 -0
  47. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/test_neldermead.py +0 -0
  48. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/test_random.py +0 -0
  49. {xopt-2.2.3 → xopt-2.3.0}/tests/generators/test_rcds.py +0 -0
  50. {xopt-2.2.3 → xopt-2.3.0}/tests/test_evaluator.py +0 -0
  51. {xopt-2.2.3 → xopt-2.3.0}/tests/test_generator.py +0 -0
  52. {xopt-2.2.3 → xopt-2.3.0}/tests/test_io.py +0 -0
  53. {xopt-2.2.3 → xopt-2.3.0}/tests/test_mpi.py +0 -0
  54. {xopt-2.2.3 → xopt-2.3.0}/tests/test_numerical_optimizer.py +0 -0
  55. {xopt-2.2.3 → xopt-2.3.0}/tests/test_perf.py +0 -0
  56. {xopt-2.2.3 → xopt-2.3.0}/tests/test_pydantic.py +0 -0
  57. {xopt-2.2.3 → xopt-2.3.0}/tests/test_utils.py +0 -0
  58. {xopt-2.2.3 → xopt-2.3.0}/tests/test_xopt.py +0 -0
  59. {xopt-2.2.3 → xopt-2.3.0}/versioneer.py +0 -0
  60. {xopt-2.2.3 → xopt-2.3.0}/xopt/__init__.py +0 -0
  61. {xopt-2.2.3 → xopt-2.3.0}/xopt/asynchronous.py +0 -0
  62. {xopt-2.2.3 → xopt-2.3.0}/xopt/base.py +0 -0
  63. {xopt-2.2.3 → xopt-2.3.0}/xopt/errors.py +0 -0
  64. {xopt-2.2.3 → xopt-2.3.0}/xopt/evaluator.py +0 -0
  65. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/__init__.py +0 -0
  66. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/base_model.py +0 -0
  67. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/bax/__init__.py +0 -0
  68. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/bax/acquisition.py +0 -0
  69. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/bax_generator.py +0 -0
  70. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/bayesian_exploration.py +0 -0
  71. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/__init__.py +0 -0
  72. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/constrained_acquisition.py +0 -0
  73. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/hessian_kernel.py +0 -0
  74. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/heteroskedastic.py +0 -0
  75. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/identity.py +0 -0
  76. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/log_acquisition_function.py +0 -0
  77. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/multi_fidelity.py +0 -0
  78. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/custom_botorch/proximal.py +0 -0
  79. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/mggpo.py +0 -0
  80. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/mobo.py +0 -0
  81. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/models/__init__.py +0 -0
  82. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/models/prior_mean.py +0 -0
  83. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/models/standard.py +0 -0
  84. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/models/time_dependent.py +0 -0
  85. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/multi_fidelity.py +0 -0
  86. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/time_dependent.py +0 -0
  87. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/turbo.py +0 -0
  88. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/bayesian/utils.py +0 -0
  89. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/es/__init__.py +0 -0
  90. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/es/extremumseeking.py +0 -0
  91. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/ga/__init__.py +0 -0
  92. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/ga/cnsga.py +0 -0
  93. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/ga/deap_creator.py +0 -0
  94. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/ga/deap_fitness_with_constraints.py +0 -0
  95. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/ga/fitness_with_constraints.py +0 -0
  96. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/random.py +0 -0
  97. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/rcds/__init__.py +0 -0
  98. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/rcds/rcds.py +0 -0
  99. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/scipy/__init__.py +0 -0
  100. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/scipy/latin_hypercube.py +0 -0
  101. {xopt-2.2.3 → xopt-2.3.0}/xopt/generators/utils.py +0 -0
  102. {xopt-2.2.3 → xopt-2.3.0}/xopt/log.py +0 -0
  103. {xopt-2.2.3 → xopt-2.3.0}/xopt/mpi/__init__.py +0 -0
  104. {xopt-2.2.3 → xopt-2.3.0}/xopt/mpi/run.py +0 -0
  105. {xopt-2.2.3 → xopt-2.3.0}/xopt/numerical_optimizer.py +0 -0
  106. {xopt-2.2.3 → xopt-2.3.0}/xopt/pydantic.py +0 -0
  107. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/__init__.py +0 -0
  108. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/benchmarking.py +0 -0
  109. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/__init__.py +0 -0
  110. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/ackley_20.py +0 -0
  111. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/modified_tnk.py +0 -0
  112. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/multi_objective.py +0 -0
  113. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/problem.py +0 -0
  114. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/rosenbrock.py +0 -0
  115. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/sinusoid_1d.py +0 -0
  116. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/sphere_100.py +0 -0
  117. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/sphere_20.py +0 -0
  118. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/sphere_50.py +0 -0
  119. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/test_functions/tnk.py +0 -0
  120. {xopt-2.2.3 → xopt-2.3.0}/xopt/resources/testing.py +0 -0
  121. {xopt-2.2.3 → xopt-2.3.0}/xopt/utils.py +0 -0
  122. {xopt-2.2.3 → xopt-2.3.0}/xopt.egg-info/dependency_links.txt +0 -0
  123. {xopt-2.2.3 → xopt-2.3.0}/xopt.egg-info/requires.txt +0 -0
  124. {xopt-2.2.3 → 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.3
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
@@ -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
 
@@ -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()
@@ -1,3 +1,4 @@
1
+ import warnings
1
2
  from copy import deepcopy
2
3
 
3
4
  import numpy as np
@@ -22,6 +23,12 @@ 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]},
@@ -125,18 +132,39 @@ class TestVOCS(object):
125
132
  assert vocs.n_outputs == 2
126
133
  assert vocs.variable_names == ["x1"]
127
134
 
128
- def test_custom_bounds(self):
135
+ def test_random_sampling_custom_bounds(self):
129
136
  vocs = deepcopy(TEST_VOCS_BASE)
130
137
 
131
138
  custom_bounds = {"x1": [0.5, 0.75], "x2": [7.5, 15.0]}
132
139
 
133
- 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
+
134
143
  random_input_data = pd.DataFrame(random_input_data)
135
144
  assert all(random_input_data["x1"] < 0.75)
136
145
  assert all(random_input_data["x1"] > 0.5)
137
146
  assert all(random_input_data["x2"] > 7.5)
138
147
  assert all(random_input_data["x2"] < 10.0)
139
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
+
140
168
  def test_duplicate_outputs(self):
141
169
  vocs = deepcopy(TEST_VOCS_BASE)
142
170
  assert vocs.output_names == ["y1", "c1"]
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-05-07T12:53:58-0400",
11
+ "date": "2024-06-18T12:58:34-0500",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "2a01895f05a19d2a26b527c4c47baaf099cd0110",
15
- "version": "2.2.3"
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
@@ -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
@@ -550,17 +555,21 @@ class BayesianGenerator(Generator, ABC):
550
555
  pass
551
556
 
552
557
  def _get_objective(self):
553
- """
554
- return default objective (scalar objective) determined by vocs
555
-
556
- If objectives are specified the returned function will weight model
557
- by +/- 1.0 according to MAXIMIZE/MINIMIZE keys in vocs.
558
-
559
- If no objectives are specified, the returned function will weight observable
560
- models by +1.0. This is used in Bayesian exploration.
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
+ )
561
569
 
562
- """
563
- return create_mc_objective(self.vocs, self._tkwargs)
570
+ return self.custom_objective
571
+ else:
572
+ return create_mc_objective(self.vocs, self._tkwargs)
564
573
 
565
574
  def _get_constraint_callables(self):
566
575
  """return constratint callable determined by vocs"""
@@ -9,6 +9,7 @@ from xopt.generators.bayesian.bayesian_generator import (
9
9
  BayesianGenerator,
10
10
  formatted_base_docstring,
11
11
  )
12
+ from xopt.generators.bayesian.objectives import CustomXoptObjective
12
13
  from xopt.generators.bayesian.utils import set_botorch_weights
13
14
 
14
15
 
@@ -22,20 +23,22 @@ class ExpectedImprovementGenerator(BayesianGenerator):
22
23
  )
23
24
 
24
25
  def _get_acquisition(self, model):
25
- objective_data = self.vocs.objective_data(self.data, "").dropna()
26
- best_f = -torch.tensor(objective_data.min().values, **self._tkwargs)
26
+ objective = self._get_objective()
27
+ best_f = self._get_best_f(self.data, objective)
27
28
 
28
- if self.n_candidates > 1:
29
+ if self.n_candidates > 1 or isinstance(objective, CustomXoptObjective):
29
30
  # MC sampling for generating multiple candidate points
30
31
  sampler = self._get_sampler(model)
31
32
  acq = qExpectedImprovement(
32
33
  model,
33
34
  best_f=best_f,
34
35
  sampler=sampler,
35
- objective=self._get_objective(),
36
+ objective=objective,
36
37
  )
37
38
  else:
38
- # analytic acquisition function for single candidate generation
39
+ # analytic acquisition function for single candidate generation with
40
+ # basic objective
41
+ # note that the analytic version cannot handle custom objectives
39
42
  weights = set_botorch_weights(self.vocs).to(**self._tkwargs)
40
43
  posterior_transform = ScalarizedPosteriorTransform(weights)
41
44
  acq = ExpectedImprovement(
@@ -43,3 +46,19 @@ class ExpectedImprovementGenerator(BayesianGenerator):
43
46
  )
44
47
 
45
48
  return acq
49
+
50
+ def _get_best_f(self, data, objective):
51
+ """get best function value for EI based on the objective"""
52
+ if isinstance(objective, CustomXoptObjective):
53
+ best_f = objective(
54
+ torch.tensor(
55
+ self.vocs.observable_data(data).to_numpy(), **self._tkwargs
56
+ )
57
+ ).max()
58
+ else:
59
+ # analytic acquisition function for single candidate generation
60
+ best_f = -torch.tensor(
61
+ self.vocs.objective_data(data).min().values, **self._tkwargs
62
+ )
63
+
64
+ return best_f
@@ -1,9 +1,14 @@
1
+ from abc import ABC, abstractmethod
1
2
  from functools import partial
3
+ from typing import Optional
2
4
 
3
5
  import torch
4
- from botorch.acquisition import GenericMCObjective
6
+ from botorch.acquisition import GenericMCObjective, MCAcquisitionObjective
5
7
  from botorch.acquisition.multi_objective import WeightedMCMultiOutputObjective
6
8
  from botorch.sampling import get_sampler
9
+ from torch import Tensor
10
+
11
+ from xopt import VOCS
7
12
 
8
13
  from xopt.generators.bayesian.custom_botorch.constrained_acquisition import (
9
14
  FeasibilityObjective,
@@ -11,6 +16,40 @@ from xopt.generators.bayesian.custom_botorch.constrained_acquisition import (
11
16
  from xopt.generators.bayesian.utils import set_botorch_weights
12
17
 
13
18
 
19
+ class CustomXoptObjective(MCAcquisitionObjective, ABC):
20
+ """
21
+ Custom objective function wrapper for use in Bayesian generators
22
+
23
+ """
24
+
25
+ def __init__(self, vocs: VOCS, *args, **kwargs):
26
+ super().__init__(*args, **kwargs)
27
+ self.vocs = vocs
28
+
29
+ @abstractmethod
30
+ def forward(self, samples: Tensor, X: Optional[Tensor] = None) -> Tensor:
31
+ r"""Evaluate the objective on the samples.
32
+
33
+ Args:
34
+ samples: A `sample_shape x batch_shape x q x m`-dim Tensors of
35
+ samples from a model posterior.
36
+ X: A `batch_shape x q x d`-dim tensor of inputs. Relevant only if
37
+ the objective depends on the inputs explicitly.
38
+
39
+ Returns:
40
+ Tensor: A `sample_shape x batch_shape x q`-dim Tensor of objective
41
+ values (assuming maximization).
42
+
43
+ This method is usually not called directly, but via the objectives.
44
+
45
+ Example:
46
+ >>> # `__call__` method:
47
+ >>> samples = sampler(posterior)
48
+ >>> outcome = mc_obj(samples)
49
+ """
50
+ pass
51
+
52
+
14
53
  def feasibility(X, model, vocs, posterior_transform=None):
15
54
  constraints = create_constraint_callables(vocs)
16
55
  posterior = model.posterior(X=X, posterior_transform=posterior_transform)
@@ -11,6 +11,7 @@ from xopt.generators.bayesian.bayesian_generator import (
11
11
  BayesianGenerator,
12
12
  formatted_base_docstring,
13
13
  )
14
+ from xopt.generators.bayesian.objectives import CustomXoptObjective
14
15
  from xopt.generators.bayesian.time_dependent import TimeDependentBayesianGenerator
15
16
  from xopt.generators.bayesian.utils import set_botorch_weights
16
17
 
@@ -50,7 +51,8 @@ beta : float, default 2.0
50
51
  )
51
52
 
52
53
  def _get_acquisition(self, model):
53
- if self.n_candidates > 1:
54
+ objective = self._get_objective()
55
+ if self.n_candidates > 1 or isinstance(objective, CustomXoptObjective):
54
56
  # MC sampling for generating multiple candidate points
55
57
  sampler = self._get_sampler(model)
56
58
  acq = qUpperConfidenceBound(
@@ -688,8 +688,12 @@ def _plot2d_prediction(
688
688
  kwargs = locals()
689
689
  axis.locator_params(axis="both", nbins=5)
690
690
  pcm = axis.pcolormesh(
691
- input_mesh[:, 0].reshape(n_grid, n_grid).numpy(),
692
- input_mesh[:, 1].reshape(n_grid, n_grid).numpy(),
691
+ input_mesh[:, vocs.variable_names.index(variable_names[0])]
692
+ .reshape(n_grid, n_grid)
693
+ .numpy(),
694
+ input_mesh[:, vocs.variable_names.index(variable_names[1])]
695
+ .reshape(n_grid, n_grid)
696
+ .numpy(),
693
697
  prediction.reshape(n_grid, n_grid),
694
698
  rasterized=True,
695
699
  )