ennbo 0.1.0__py3-none-any.whl → 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. enn/__init__.py +25 -13
  2. enn/benchmarks/__init__.py +3 -0
  3. enn/benchmarks/ackley.py +5 -0
  4. enn/benchmarks/ackley_class.py +17 -0
  5. enn/benchmarks/ackley_core.py +12 -0
  6. enn/benchmarks/double_ackley.py +24 -0
  7. enn/enn/candidates.py +14 -0
  8. enn/enn/conditional_posterior_draw_internals.py +15 -0
  9. enn/enn/draw_internals.py +15 -0
  10. enn/enn/enn.py +16 -229
  11. enn/enn/enn_class.py +423 -0
  12. enn/enn/enn_conditional.py +325 -0
  13. enn/enn/enn_fit.py +77 -76
  14. enn/enn/enn_hash.py +79 -0
  15. enn/enn/enn_index.py +92 -0
  16. enn/enn/enn_like_protocol.py +35 -0
  17. enn/enn/enn_normal.py +3 -3
  18. enn/enn/enn_params.py +3 -9
  19. enn/enn/enn_params_class.py +24 -0
  20. enn/enn/enn_util.py +79 -37
  21. enn/enn/neighbor_data.py +14 -0
  22. enn/enn/neighbors.py +14 -0
  23. enn/enn/posterior_flags.py +8 -0
  24. enn/enn/weighted_stats.py +14 -0
  25. enn/turbo/components/__init__.py +41 -0
  26. enn/turbo/components/acquisition.py +13 -0
  27. enn/turbo/components/acquisition_optimizer_protocol.py +19 -0
  28. enn/turbo/components/builder.py +22 -0
  29. enn/turbo/components/chebyshev_incumbent_selector.py +76 -0
  30. enn/turbo/components/enn_surrogate.py +115 -0
  31. enn/turbo/components/gp_surrogate.py +144 -0
  32. enn/turbo/components/hnr_acq_optimizer.py +83 -0
  33. enn/turbo/components/incumbent_selector.py +11 -0
  34. enn/turbo/components/incumbent_selector_protocol.py +16 -0
  35. enn/turbo/components/no_incumbent_selector.py +21 -0
  36. enn/turbo/components/no_surrogate.py +49 -0
  37. enn/turbo/components/pareto_acq_optimizer.py +49 -0
  38. enn/turbo/components/posterior_result.py +12 -0
  39. enn/turbo/components/protocols.py +13 -0
  40. enn/turbo/components/random_acq_optimizer.py +21 -0
  41. enn/turbo/components/scalar_incumbent_selector.py +39 -0
  42. enn/turbo/components/surrogate_protocol.py +32 -0
  43. enn/turbo/components/surrogate_result.py +12 -0
  44. enn/turbo/components/surrogates.py +5 -0
  45. enn/turbo/components/thompson_acq_optimizer.py +49 -0
  46. enn/turbo/components/trust_region_protocol.py +24 -0
  47. enn/turbo/components/ucb_acq_optimizer.py +49 -0
  48. enn/turbo/config/__init__.py +87 -0
  49. enn/turbo/config/acq_type.py +8 -0
  50. enn/turbo/config/acquisition.py +26 -0
  51. enn/turbo/config/base.py +4 -0
  52. enn/turbo/config/candidate_gen_config.py +49 -0
  53. enn/turbo/config/candidate_rv.py +7 -0
  54. enn/turbo/config/draw_acquisition_config.py +14 -0
  55. enn/turbo/config/enn_index_driver.py +6 -0
  56. enn/turbo/config/enn_surrogate_config.py +42 -0
  57. enn/turbo/config/enums.py +7 -0
  58. enn/turbo/config/factory.py +118 -0
  59. enn/turbo/config/gp_surrogate_config.py +14 -0
  60. enn/turbo/config/hnr_optimizer_config.py +7 -0
  61. enn/turbo/config/init_config.py +17 -0
  62. enn/turbo/config/init_strategies/__init__.py +9 -0
  63. enn/turbo/config/init_strategies/hybrid_init.py +23 -0
  64. enn/turbo/config/init_strategies/init_strategy.py +19 -0
  65. enn/turbo/config/init_strategies/lhd_only_init.py +24 -0
  66. enn/turbo/config/morbo_tr_config.py +82 -0
  67. enn/turbo/config/nds_optimizer_config.py +7 -0
  68. enn/turbo/config/no_surrogate_config.py +14 -0
  69. enn/turbo/config/no_tr_config.py +31 -0
  70. enn/turbo/config/optimizer_config.py +72 -0
  71. enn/turbo/config/pareto_acquisition_config.py +14 -0
  72. enn/turbo/config/raasp_driver.py +6 -0
  73. enn/turbo/config/raasp_optimizer_config.py +7 -0
  74. enn/turbo/config/random_acquisition_config.py +14 -0
  75. enn/turbo/config/rescalarize.py +7 -0
  76. enn/turbo/config/surrogate.py +12 -0
  77. enn/turbo/config/trust_region.py +34 -0
  78. enn/turbo/config/turbo_tr_config.py +71 -0
  79. enn/turbo/config/ucb_acquisition_config.py +14 -0
  80. enn/turbo/config/validation.py +45 -0
  81. enn/turbo/hypervolume.py +30 -0
  82. enn/turbo/impl_helpers.py +68 -0
  83. enn/turbo/morbo_trust_region.py +250 -0
  84. enn/turbo/no_trust_region.py +58 -0
  85. enn/turbo/optimizer.py +300 -0
  86. enn/turbo/optimizer_config.py +8 -0
  87. enn/turbo/proposal.py +46 -39
  88. enn/turbo/sampling.py +21 -0
  89. enn/turbo/strategies/__init__.py +9 -0
  90. enn/turbo/strategies/lhd_only_strategy.py +36 -0
  91. enn/turbo/strategies/optimization_strategy.py +19 -0
  92. enn/turbo/strategies/turbo_hybrid_strategy.py +124 -0
  93. enn/turbo/tr_helpers.py +202 -0
  94. enn/turbo/turbo_gp.py +9 -2
  95. enn/turbo/turbo_gp_base.py +0 -1
  96. enn/turbo/turbo_gp_fit.py +187 -0
  97. enn/turbo/turbo_gp_noisy.py +0 -1
  98. enn/turbo/turbo_optimizer_utils.py +98 -0
  99. enn/turbo/turbo_trust_region.py +129 -63
  100. enn/turbo/turbo_utils.py +144 -117
  101. enn/turbo/types/__init__.py +7 -0
  102. enn/turbo/types/appendable_array.py +85 -0
  103. enn/turbo/types/gp_data_prep.py +13 -0
  104. enn/turbo/types/gp_fit_result.py +11 -0
  105. enn/turbo/types/obs_lists.py +10 -0
  106. enn/turbo/types/prepare_ask_result.py +14 -0
  107. enn/turbo/types/tell_inputs.py +14 -0
  108. {ennbo-0.1.0.dist-info → ennbo-0.1.7.dist-info}/METADATA +22 -14
  109. ennbo-0.1.7.dist-info/RECORD +111 -0
  110. enn/enn/__init__.py +0 -4
  111. enn/turbo/__init__.py +0 -11
  112. enn/turbo/base_turbo_impl.py +0 -98
  113. enn/turbo/lhd_only_impl.py +0 -42
  114. enn/turbo/turbo_config.py +0 -28
  115. enn/turbo/turbo_enn_impl.py +0 -176
  116. enn/turbo/turbo_mode.py +0 -10
  117. enn/turbo/turbo_mode_impl.py +0 -67
  118. enn/turbo/turbo_one_impl.py +0 -163
  119. enn/turbo/turbo_optimizer.py +0 -337
  120. enn/turbo/turbo_zero_impl.py +0 -24
  121. ennbo-0.1.0.dist-info/RECORD +0 -27
  122. {ennbo-0.1.0.dist-info → ennbo-0.1.7.dist-info}/WHEEL +0 -0
  123. {ennbo-0.1.0.dist-info → ennbo-0.1.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+ import time
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+ import numpy as np
6
+ from ..sampling import draw_lhd
7
+ from ..types.appendable_array import AppendableArray
8
+ from .optimization_strategy import OptimizationStrategy
9
+
10
+ if TYPE_CHECKING:
11
+ from ..optimizer import Optimizer
12
+ from ..types import TellInputs
13
+
14
+
15
+ @dataclass
16
+ class TurboHybridStrategy(OptimizationStrategy):
17
+ _bounds: np.ndarray
18
+ _num_dim: int
19
+ _rng: object
20
+ _num_init: int
21
+ _init_lhd: np.ndarray
22
+ _init_idx: int = 0
23
+
24
+ @classmethod
25
+ def create(
26
+ cls, *, bounds: np.ndarray, rng: object, num_init: int | None
27
+ ) -> TurboHybridStrategy:
28
+ from numpy.random import Generator
29
+
30
+ bounds = np.asarray(bounds, dtype=float)
31
+ if bounds.ndim != 2 or bounds.shape[1] != 2:
32
+ raise ValueError(f"bounds must be (d, 2), got {bounds.shape}")
33
+ num_dim = int(bounds.shape[0])
34
+ if not isinstance(rng, Generator):
35
+ raise TypeError("rng must be a numpy.random.Generator")
36
+ n_init = int(num_init if num_init is not None else 2 * num_dim)
37
+ if n_init <= 0:
38
+ raise ValueError(f"num_init must be > 0, got {n_init}")
39
+ init_lhd = draw_lhd(bounds=bounds, num_arms=n_init, rng=rng)
40
+ return cls(
41
+ _bounds=bounds,
42
+ _num_dim=num_dim,
43
+ _rng=rng,
44
+ _num_init=n_init,
45
+ _init_lhd=init_lhd,
46
+ )
47
+
48
+ def _reset_init(self) -> None:
49
+ from numpy.random import Generator
50
+
51
+ if not isinstance(self._rng, Generator):
52
+ raise TypeError("rng must be a numpy.random.Generator")
53
+ self._init_lhd = draw_lhd(
54
+ bounds=self._bounds, num_arms=self._num_init, rng=self._rng
55
+ )
56
+ self._init_idx = 0
57
+
58
+ def _get_init_points(self, num_arms: int, *, fallback_fn=None) -> np.ndarray:
59
+ num_arms = int(num_arms)
60
+ num_to_return = min(num_arms, self._num_init - self._init_idx)
61
+ result = self._init_lhd[self._init_idx : self._init_idx + num_to_return]
62
+ self._init_idx += num_to_return
63
+ if num_to_return < num_arms:
64
+ extra = (
65
+ fallback_fn
66
+ or (lambda n: draw_lhd(bounds=self._bounds, num_arms=n, rng=self._rng))
67
+ )(num_arms - num_to_return)
68
+ result = np.vstack([result, extra])
69
+ return result
70
+
71
+ def ask(self, opt: Optimizer, num_arms: int) -> np.ndarray:
72
+ if opt._tr_state.needs_restart():
73
+ opt._tr_state.restart(opt._rng)
74
+ opt._restart_generation += 1
75
+ opt._x_obs = AppendableArray()
76
+ opt._y_obs = AppendableArray()
77
+ opt._yvar_obs = AppendableArray()
78
+ opt._y_tr_list = []
79
+ opt._incumbent_idx = None
80
+ opt._incumbent_x_unit = None
81
+ opt._incumbent_y_scalar = None
82
+ self._reset_init()
83
+ return self._get_init_points(num_arms)
84
+ if self._init_idx < self._num_init:
85
+
86
+ def fallback(n: int) -> np.ndarray:
87
+ return opt._ask_normal(n, is_fallback=True)
88
+
89
+ return self._get_init_points(
90
+ num_arms,
91
+ fallback_fn=fallback if len(opt._x_obs) > 0 else None,
92
+ )
93
+ if len(opt._x_obs) == 0:
94
+ return draw_lhd(bounds=self._bounds, num_arms=num_arms, rng=self._rng)
95
+ opt._tr_state.validate_request(int(num_arms))
96
+ return opt._ask_normal(int(num_arms))
97
+
98
+ def init_progress(self) -> tuple[int, int] | None:
99
+ return (int(self._init_idx), int(self._num_init))
100
+
101
+ def tell(
102
+ self, opt: Optimizer, inputs: TellInputs, *, x_unit: np.ndarray
103
+ ) -> np.ndarray:
104
+ x_all = opt._x_obs.view()
105
+ y_all = opt._y_obs.view()
106
+ y_var_all = opt._yvar_obs.view() if len(opt._yvar_obs) > 0 else None
107
+ t0 = time.perf_counter()
108
+ opt._surrogate.fit(
109
+ x_all, y_all, y_var_all, num_steps=opt._gp_num_steps, rng=opt._rng
110
+ )
111
+ opt._dt_fit = time.perf_counter() - t0
112
+ opt._y_tr_list = y_all.tolist()
113
+ opt._update_incumbent()
114
+ try:
115
+ new_posterior = opt._surrogate.predict(x_unit)
116
+ y_estimate = np.asarray(new_posterior.mu, dtype=float)
117
+ except RuntimeError:
118
+ y_estimate = np.asarray(inputs.y, dtype=float)
119
+ y_incumbent = opt._incumbent_y_scalar
120
+ y_obs = y_all
121
+ opt._tr_state.update(y_obs, y_incumbent)
122
+ if opt._trailing_obs is not None:
123
+ opt._trim_trailing_obs()
124
+ return y_estimate
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from .config.enums import CandidateRV, RAASPDriver
6
+
7
+ if TYPE_CHECKING:
8
+ import numpy as np
9
+ from numpy.random import Generator
10
+ from scipy.stats._qmc import QMCEngine
11
+
12
+
13
+ def compute_full_box_bounds_1d(
14
+ x_center: np.ndarray,
15
+ ) -> tuple[np.ndarray, np.ndarray]:
16
+ import numpy as np
17
+
18
+ lb = np.zeros_like(x_center, dtype=float)
19
+ ub = np.ones_like(x_center, dtype=float)
20
+ return lb, ub
21
+
22
+
23
+ def get_single_incumbent_index(
24
+ selector,
25
+ y: np.ndarray,
26
+ rng: Generator,
27
+ mu: np.ndarray | None = None,
28
+ ) -> np.ndarray:
29
+ import numpy as np
30
+
31
+ y = np.asarray(y, dtype=float)
32
+ if y.size == 0:
33
+ return np.array([], dtype=int)
34
+ best_idx = selector.select(y, mu, rng)
35
+ return np.array([best_idx])
36
+
37
+
38
+ def get_incumbent_index(
39
+ selector,
40
+ y: np.ndarray,
41
+ rng: Generator,
42
+ mu: np.ndarray | None = None,
43
+ ) -> int:
44
+ import numpy as np
45
+
46
+ y = np.asarray(y, dtype=float)
47
+ if y.size == 0:
48
+ raise ValueError("y is empty")
49
+ return int(selector.select(y, mu, rng))
50
+
51
+
52
+ def get_scalar_incumbent_value(
53
+ selector,
54
+ y_obs: np.ndarray,
55
+ rng: Generator,
56
+ *,
57
+ mu_obs: np.ndarray | None = None,
58
+ ) -> np.ndarray:
59
+ import numpy as np
60
+
61
+ y = np.asarray(y_obs, dtype=float)
62
+ if y.size == 0:
63
+ return np.array([], dtype=float)
64
+ idx = get_incumbent_index(selector, y, rng, mu=mu_obs)
65
+ use_mu = bool(getattr(selector, "noise_aware", False))
66
+ values = mu_obs if use_mu else y
67
+ if values is None:
68
+ raise ValueError("noise_aware incumbent selection requires mu_obs")
69
+ v = np.asarray(values, dtype=float)
70
+ if v.ndim == 2:
71
+ value = float(v[idx, 0])
72
+ elif v.ndim == 1:
73
+ value = float(v[idx])
74
+ else:
75
+ raise ValueError(v.shape)
76
+ return np.array([value], dtype=float)
77
+
78
+
79
+ class ScalarIncumbentMixin:
80
+ incumbent_selector: Any
81
+
82
+ def get_incumbent_index(
83
+ self,
84
+ y: np.ndarray | Any,
85
+ rng: Generator,
86
+ mu: np.ndarray | None = None,
87
+ ) -> int:
88
+ return get_incumbent_index(self.incumbent_selector, y, rng, mu=mu)
89
+
90
+ def get_incumbent_value(
91
+ self,
92
+ y_obs: np.ndarray | Any,
93
+ rng: Generator,
94
+ mu_obs: np.ndarray | None = None,
95
+ ) -> np.ndarray:
96
+ return get_scalar_incumbent_value(
97
+ self.incumbent_selector, y_obs, rng, mu_obs=mu_obs
98
+ )
99
+
100
+
101
+ def generate_tr_candidates_orig(
102
+ compute_bounds_1d: Any,
103
+ x_center: np.ndarray,
104
+ lengthscales: np.ndarray | None,
105
+ num_candidates: int,
106
+ *,
107
+ rng: Generator,
108
+ candidate_rv: CandidateRV = CandidateRV.SOBOL,
109
+ sobol_engine: QMCEngine | None = None,
110
+ ) -> np.ndarray:
111
+ from .turbo_utils import (
112
+ generate_raasp_candidates,
113
+ generate_raasp_candidates_uniform,
114
+ )
115
+
116
+ lb, ub = compute_bounds_1d(x_center, lengthscales)
117
+ if candidate_rv == CandidateRV.SOBOL:
118
+ if sobol_engine is None:
119
+ raise ValueError(
120
+ "sobol_engine is required when candidate_rv=CandidateRV.SOBOL"
121
+ )
122
+ return generate_raasp_candidates(
123
+ x_center, lb, ub, num_candidates, rng=rng, sobol_engine=sobol_engine
124
+ )
125
+ if candidate_rv == CandidateRV.UNIFORM:
126
+ return generate_raasp_candidates_uniform(
127
+ x_center, lb, ub, num_candidates, rng=rng
128
+ )
129
+ raise ValueError(candidate_rv)
130
+
131
+
132
+ def generate_tr_candidates_fast(
133
+ compute_bounds_1d: Any,
134
+ x_center: np.ndarray,
135
+ lengthscales: np.ndarray | None,
136
+ num_candidates: int,
137
+ *,
138
+ rng: Generator,
139
+ candidate_rv: CandidateRV,
140
+ num_pert: int,
141
+ ) -> np.ndarray:
142
+ import numpy as np
143
+ from scipy.stats import qmc
144
+
145
+ lb, ub = compute_bounds_1d(x_center, lengthscales)
146
+ num_dim = x_center.shape[-1]
147
+
148
+ candidates = np.empty((num_candidates, num_dim), dtype=float)
149
+ candidates[:] = x_center
150
+
151
+ prob_perturb = min(num_pert / num_dim, 1.0)
152
+ ks = rng.binomial(num_dim, prob_perturb, size=num_candidates)
153
+ ks = np.maximum(ks, 1)
154
+ max_k = int(np.max(ks))
155
+
156
+ if candidate_rv == CandidateRV.SOBOL:
157
+ sobol = qmc.Sobol(d=max_k, scramble=True, seed=int(rng.integers(0, 2**31)))
158
+ samples = sobol.random(num_candidates)
159
+ elif candidate_rv == CandidateRV.UNIFORM:
160
+ samples = rng.random((num_candidates, max_k))
161
+ else:
162
+ raise ValueError(candidate_rv)
163
+
164
+ for i in range(num_candidates):
165
+ k = ks[i]
166
+ idx = rng.choice(num_dim, size=k, replace=False)
167
+ candidates[i, idx] = lb[idx] + (ub[idx] - lb[idx]) * samples[i, :k]
168
+
169
+ return candidates
170
+
171
+
172
+ def generate_tr_candidates(
173
+ compute_bounds_1d: Any,
174
+ x_center: np.ndarray,
175
+ lengthscales: np.ndarray | None,
176
+ num_candidates: int,
177
+ *,
178
+ rng: Generator,
179
+ candidate_rv: CandidateRV,
180
+ sobol_engine: QMCEngine | None,
181
+ raasp_driver: RAASPDriver,
182
+ num_pert: int,
183
+ ) -> np.ndarray:
184
+ if raasp_driver == RAASPDriver.FAST:
185
+ return generate_tr_candidates_fast(
186
+ compute_bounds_1d,
187
+ x_center,
188
+ lengthscales,
189
+ num_candidates,
190
+ rng=rng,
191
+ candidate_rv=candidate_rv,
192
+ num_pert=num_pert,
193
+ )
194
+ return generate_tr_candidates_orig(
195
+ compute_bounds_1d,
196
+ x_center,
197
+ lengthscales,
198
+ num_candidates,
199
+ rng=rng,
200
+ candidate_rv=candidate_rv,
201
+ sobol_engine=sobol_engine,
202
+ )
enn/turbo/turbo_gp.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from __future__ import annotations
2
-
3
2
  from .turbo_gp_base import TurboGPBase
4
3
 
5
4
 
@@ -13,17 +12,25 @@ class TurboGP(TurboGPBase):
13
12
  outputscale_constraint,
14
13
  ard_dims: int,
15
14
  ) -> None:
15
+ import torch
16
16
  from gpytorch.kernels import MaternKernel, ScaleKernel
17
17
  from gpytorch.means import ConstantMean
18
18
 
19
19
  super().__init__(train_x, train_y, likelihood)
20
- self.mean_module = ConstantMean()
20
+ batch_shape = (
21
+ torch.Size(train_y.shape[:-1])
22
+ if getattr(train_y, "ndim", 0) > 1
23
+ else torch.Size()
24
+ )
25
+ self.mean_module = ConstantMean(batch_shape=batch_shape)
21
26
  base_kernel = MaternKernel(
22
27
  nu=2.5,
23
28
  ard_num_dims=ard_dims,
29
+ batch_shape=batch_shape,
24
30
  lengthscale_constraint=lengthscale_constraint,
25
31
  )
26
32
  self.covar_module = ScaleKernel(
27
33
  base_kernel,
34
+ batch_shape=batch_shape,
28
35
  outputscale_constraint=outputscale_constraint,
29
36
  )
@@ -1,5 +1,4 @@
1
1
  from __future__ import annotations
2
-
3
2
  from typing import TYPE_CHECKING, Any
4
3
 
5
4
  if TYPE_CHECKING:
@@ -0,0 +1,187 @@
1
+ from __future__ import annotations
2
+ from typing import Any
3
+ from enn.enn.enn_util import standardize_y
4
+ from .types import GPDataPrep, GPFitResult
5
+
6
+
7
+ def _prepare_gp_data(
8
+ x_obs_list: list, y_obs_list: list, yvar_obs_list: list | None
9
+ ) -> GPDataPrep:
10
+ import numpy as np
11
+ import torch
12
+
13
+ x = np.asarray(x_obs_list, dtype=float)
14
+ y = np.asarray(y_obs_list, dtype=float)
15
+ if y.ndim not in (1, 2):
16
+ raise ValueError(y.shape)
17
+ is_multi = y.ndim == 2 and y.shape[1] > 1
18
+ if yvar_obs_list is not None:
19
+ if len(yvar_obs_list) != len(y_obs_list):
20
+ raise ValueError(
21
+ f"yvar_obs_list length {len(yvar_obs_list)} != y_obs_list length {len(y_obs_list)}"
22
+ )
23
+ if is_multi:
24
+ raise ValueError("yvar_obs_list not supported for multi-output GP")
25
+ if is_multi:
26
+ y_mean, y_std = y.mean(axis=0), y.std(axis=0)
27
+ y_std = np.where(y_std < 1e-6, 1.0, y_std)
28
+ z = (y - y_mean) / y_std
29
+ train_y = torch.as_tensor(z.T, dtype=torch.float64)
30
+ else:
31
+ y_mean, y_std = standardize_y(y)
32
+ z = (y - y_mean) / y_std
33
+ train_y = torch.as_tensor(z, dtype=torch.float64)
34
+ return GPDataPrep(
35
+ train_x=torch.as_tensor(x, dtype=torch.float64),
36
+ train_y=train_y,
37
+ is_multi=is_multi,
38
+ y_mean=y_mean,
39
+ y_std=y_std,
40
+ y_raw=y,
41
+ )
42
+
43
+
44
+ def _build_gp_model(
45
+ train_x: Any,
46
+ train_y: Any,
47
+ is_multi: bool,
48
+ num_dim: int,
49
+ *,
50
+ yvar_obs_list: list | None,
51
+ gp_y_std: Any,
52
+ y: Any,
53
+ ) -> tuple[Any, Any]:
54
+ import numpy as np
55
+ import torch
56
+ from gpytorch.constraints import Interval
57
+ from gpytorch.likelihoods import GaussianLikelihood
58
+ from .turbo_gp import TurboGP
59
+ from .turbo_gp_noisy import TurboGPNoisy
60
+
61
+ ls_constr, os_constr = Interval(0.005, 2.0), Interval(0.05, 20.0)
62
+ if yvar_obs_list is not None:
63
+ y_var = np.asarray(yvar_obs_list, dtype=float)
64
+ train_y_var = torch.as_tensor(y_var / (gp_y_std**2), dtype=torch.float64)
65
+ model = TurboGPNoisy(
66
+ train_x=train_x,
67
+ train_y=train_y,
68
+ train_y_var=train_y_var,
69
+ lengthscale_constraint=ls_constr,
70
+ outputscale_constraint=os_constr,
71
+ ard_dims=num_dim,
72
+ ).to(dtype=train_x.dtype)
73
+ return model, model.likelihood
74
+ noise_constr = Interval(5e-4, 0.2)
75
+ num_out = int(y.shape[1]) if is_multi else None
76
+ if is_multi:
77
+ likelihood = GaussianLikelihood(
78
+ noise_constraint=noise_constr, batch_shape=torch.Size([num_out])
79
+ ).to(dtype=train_y.dtype)
80
+ else:
81
+ likelihood = GaussianLikelihood(noise_constraint=noise_constr).to(
82
+ dtype=train_y.dtype
83
+ )
84
+ model = TurboGP(
85
+ train_x=train_x,
86
+ train_y=train_y,
87
+ likelihood=likelihood,
88
+ lengthscale_constraint=ls_constr,
89
+ outputscale_constraint=os_constr,
90
+ ard_dims=num_dim,
91
+ ).to(dtype=train_x.dtype)
92
+ likelihood.noise = (
93
+ torch.full((num_out,), 0.005, dtype=train_y.dtype)
94
+ if is_multi
95
+ else torch.tensor(0.005, dtype=train_y.dtype)
96
+ )
97
+ return model, likelihood
98
+
99
+
100
+ def _init_gp_hyperparams(
101
+ model: Any, is_multi: bool, num_dim: int, num_out: int | None, dtype: Any
102
+ ) -> None:
103
+ import torch
104
+
105
+ if is_multi:
106
+ model.covar_module.outputscale = torch.ones(num_out, dtype=dtype)
107
+ model.covar_module.base_kernel.lengthscale = torch.full(
108
+ (num_out, 1, num_dim), 0.5, dtype=dtype
109
+ )
110
+ else:
111
+ model.covar_module.outputscale = torch.tensor(1.0, dtype=dtype)
112
+ model.covar_module.base_kernel.lengthscale = torch.full(
113
+ (num_dim,), 0.5, dtype=dtype
114
+ )
115
+
116
+
117
+ def _train_gp(
118
+ model: Any, likelihood: Any, train_x: Any, train_y: Any, num_steps: int
119
+ ) -> None:
120
+ import torch
121
+ from gpytorch.mlls import ExactMarginalLogLikelihood
122
+
123
+ model.train()
124
+ likelihood.train()
125
+ mll = ExactMarginalLogLikelihood(likelihood, model)
126
+ optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
127
+ for _ in range(num_steps):
128
+ optimizer.zero_grad()
129
+ loss = -mll(model(train_x), train_y)
130
+ (loss.sum() if loss.ndim != 0 else loss).backward()
131
+ optimizer.step()
132
+ model.eval()
133
+ likelihood.eval()
134
+
135
+
136
+ def fit_gp(
137
+ x_obs_list: list[float] | list[list[float]],
138
+ y_obs_list: list[float] | list[list[float]],
139
+ num_dim: int,
140
+ *,
141
+ yvar_obs_list: list[float] | None = None,
142
+ num_steps: int = 50,
143
+ ) -> GPFitResult:
144
+ import numpy as np
145
+
146
+ x = np.asarray(x_obs_list, dtype=float)
147
+ y = np.asarray(y_obs_list, dtype=float)
148
+ n, is_multi = x.shape[0], y.ndim == 2 and y.shape[1] > 1
149
+ if n == 0:
150
+ return (
151
+ GPFitResult(
152
+ model=None,
153
+ likelihood=None,
154
+ y_mean=np.zeros(y.shape[1]),
155
+ y_std=np.ones(y.shape[1]),
156
+ )
157
+ if is_multi
158
+ else GPFitResult(model=None, likelihood=None, y_mean=0.0, y_std=1.0)
159
+ )
160
+ if n == 1 and is_multi:
161
+ return GPFitResult(
162
+ model=None,
163
+ likelihood=None,
164
+ y_mean=y[0].copy(),
165
+ y_std=np.ones(int(y.shape[1]), dtype=float),
166
+ )
167
+ gp_data = _prepare_gp_data(x_obs_list, y_obs_list, yvar_obs_list)
168
+ model, likelihood = _build_gp_model(
169
+ gp_data.train_x,
170
+ gp_data.train_y,
171
+ gp_data.is_multi,
172
+ num_dim,
173
+ yvar_obs_list=yvar_obs_list,
174
+ gp_y_std=gp_data.y_std,
175
+ y=gp_data.y_raw,
176
+ )
177
+ _init_gp_hyperparams(
178
+ model,
179
+ gp_data.is_multi,
180
+ num_dim,
181
+ int(gp_data.y_raw.shape[1]) if gp_data.is_multi else None,
182
+ gp_data.train_x.dtype,
183
+ )
184
+ _train_gp(model, likelihood, gp_data.train_x, gp_data.train_y, num_steps)
185
+ return GPFitResult(
186
+ model=model, likelihood=likelihood, y_mean=gp_data.y_mean, y_std=gp_data.y_std
187
+ )
@@ -1,5 +1,4 @@
1
1
  from __future__ import annotations
2
-
3
2
  from .turbo_gp_base import TurboGPBase
4
3
 
5
4
 
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+ from .types import ObsLists, TellInputs
4
+
5
+ if TYPE_CHECKING:
6
+ import numpy as np
7
+
8
+
9
+ def sobol_seed_for_state(
10
+ seed_base: int, *, restart_generation: int, n_obs: int, num_arms: int
11
+ ) -> int:
12
+ mask64 = (1 << 64) - 1
13
+ x = int(seed_base) & mask64
14
+ x ^= (int(restart_generation) + 1) * 0xD1342543DE82EF95 & mask64
15
+ x ^= (int(n_obs) + 1) * 0x9E3779B97F4A7C15 & mask64
16
+ x ^= (int(num_arms) + 1) * 0xBF58476D1CE4E5B9 & mask64
17
+ x = (x + 0x9E3779B97F4A7C15) & mask64
18
+ z = x
19
+ z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9 & mask64
20
+ z = (z ^ (z >> 27)) * 0x94D049BB133111EB & mask64
21
+ z = z ^ (z >> 31)
22
+ return int(z & 0xFFFFFFFF)
23
+
24
+
25
+ def reset_timing(opt: object) -> None:
26
+ setattr(opt, "_dt_fit", 0.0)
27
+ setattr(opt, "_dt_gen", 0.0)
28
+ setattr(opt, "_dt_sel", 0.0)
29
+
30
+
31
+ def validate_tell_inputs(
32
+ x: np.ndarray, y: np.ndarray, y_var: np.ndarray | None, num_dim: int
33
+ ) -> TellInputs:
34
+ import numpy as np
35
+
36
+ x = np.asarray(x, dtype=float)
37
+ y = np.asarray(y, dtype=float)
38
+ if x.ndim != 2 or x.shape[1] != num_dim:
39
+ raise ValueError(x.shape)
40
+ if y.ndim == 2:
41
+ if y.shape[0] != x.shape[0]:
42
+ raise ValueError((x.shape, y.shape))
43
+ num_metrics = y.shape[1]
44
+ elif y.ndim == 1:
45
+ if y.shape[0] != x.shape[0]:
46
+ raise ValueError((x.shape, y.shape))
47
+ num_metrics = 1
48
+ else:
49
+ raise ValueError(y.shape)
50
+ if y_var is not None:
51
+ y_var = np.asarray(y_var, dtype=float)
52
+ if y_var.shape != y.shape:
53
+ raise ValueError((y.shape, y_var.shape))
54
+ return TellInputs(x=x, y=y, y_var=y_var, num_metrics=num_metrics)
55
+
56
+
57
+ def trim_trailing_observations(
58
+ x_obs_list: list,
59
+ y_obs_list: list,
60
+ y_tr_list: list,
61
+ yvar_obs_list: list,
62
+ *,
63
+ trailing_obs: int,
64
+ incumbent_indices: np.ndarray,
65
+ ) -> ObsLists:
66
+ import numpy as np
67
+
68
+ num_total = len(x_obs_list)
69
+ if num_total <= trailing_obs:
70
+ return ObsLists(
71
+ x_obs=x_obs_list,
72
+ y_obs=y_obs_list,
73
+ y_tr=y_tr_list,
74
+ yvar_obs=yvar_obs_list,
75
+ )
76
+ start_idx = max(0, num_total - trailing_obs)
77
+ recent_indices = set(range(start_idx, num_total))
78
+ keep_indices = set(incumbent_indices.tolist()) | recent_indices
79
+ if len(keep_indices) > trailing_obs:
80
+ keep_indices = set(incumbent_indices.tolist())
81
+ remaining_slots = trailing_obs - len(keep_indices)
82
+ if remaining_slots > 0:
83
+ recent_non_incumbent = [
84
+ i for i in range(num_total - 1, -1, -1) if i not in keep_indices
85
+ ][:remaining_slots]
86
+ keep_indices.update(recent_non_incumbent)
87
+ indices = np.array(sorted(keep_indices), dtype=int)
88
+ x_array = np.asarray(x_obs_list, dtype=float)
89
+ y_obs_array = np.asarray(y_obs_list, dtype=float)
90
+ y_tr_array = np.asarray(y_tr_list, dtype=float)
91
+ new_x = x_array[indices].tolist()
92
+ new_y_obs = y_obs_array[indices].tolist()
93
+ new_y_tr = y_tr_array[indices].tolist() if y_tr_array.size > 0 else []
94
+ new_yvar = yvar_obs_list
95
+ if len(yvar_obs_list) == len(y_obs_array):
96
+ yvar_array = np.asarray(yvar_obs_list, dtype=float)
97
+ new_yvar = yvar_array[indices].tolist()
98
+ return ObsLists(x_obs=new_x, y_obs=new_y_obs, y_tr=new_y_tr, yvar_obs=new_yvar)