ennbo 0.1.2__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 -269
  11. enn/enn/enn_class.py +423 -0
  12. enn/enn/enn_conditional.py +325 -0
  13. enn/enn/enn_fit.py +69 -70
  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 +0 -1
  18. enn/enn/enn_params.py +3 -22
  19. enn/enn/enn_params_class.py +24 -0
  20. enn/enn/enn_util.py +60 -46
  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 +131 -70
  84. enn/turbo/no_trust_region.py +32 -39
  85. enn/turbo/optimizer.py +300 -0
  86. enn/turbo/optimizer_config.py +8 -0
  87. enn/turbo/proposal.py +36 -38
  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 +0 -1
  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 +126 -58
  100. enn/turbo/turbo_utils.py +98 -161
  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.2.dist-info → ennbo-0.1.7.dist-info}/METADATA +18 -11
  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 -144
  113. enn/turbo/lhd_only_impl.py +0 -49
  114. enn/turbo/turbo_config.py +0 -72
  115. enn/turbo/turbo_enn_impl.py +0 -201
  116. enn/turbo/turbo_mode.py +0 -10
  117. enn/turbo/turbo_mode_impl.py +0 -76
  118. enn/turbo/turbo_one_impl.py +0 -302
  119. enn/turbo/turbo_optimizer.py +0 -525
  120. enn/turbo/turbo_zero_impl.py +0 -29
  121. ennbo-0.1.2.dist-info/RECORD +0 -29
  122. {ennbo-0.1.2.dist-info → ennbo-0.1.7.dist-info}/WHEEL +0 -0
  123. {ennbo-0.1.2.dist-info → ennbo-0.1.7.dist-info}/licenses/LICENSE +0 -0
@@ -2,47 +2,60 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
+ from .tr_helpers import ScalarIncumbentMixin
6
+
5
7
  if TYPE_CHECKING:
6
8
  import numpy as np
7
9
  from numpy.random import Generator
8
10
  from scipy.stats._qmc import QMCEngine
9
11
 
10
- from .turbo_trust_region import TurboTrustRegion
12
+ from .config.morbo_tr_config import MorboTRConfig
13
+ from .config.rescalarize import Rescalarize
14
+
15
+ from .config.enums import CandidateRV, RAASPDriver
11
16
 
12
17
 
13
- class MorboTrustRegion:
18
+ class MorboTrustRegion(ScalarIncumbentMixin):
14
19
  def __init__(
15
20
  self,
21
+ config: MorboTRConfig,
16
22
  num_dim: int,
17
- num_arms: int,
18
- num_metrics: int,
19
23
  *,
20
24
  rng: Generator,
25
+ candidate_rv: CandidateRV = CandidateRV.SOBOL,
21
26
  ) -> None:
22
- import numpy as np
23
-
24
- self._tr = TurboTrustRegion(num_dim=num_dim, num_arms=num_arms)
27
+ from .components.incumbent_selector import ChebyshevIncumbentSelector
28
+ from .config.turbo_tr_config import TurboTRConfig
29
+ from .turbo_trust_region import TurboTrustRegion
30
+
31
+ self._config = config
32
+ self._candidate_rv = candidate_rv
33
+ inner_config = TurboTRConfig(length=config.length)
34
+ self._tr = TurboTrustRegion(
35
+ config=inner_config,
36
+ num_dim=num_dim,
37
+ )
25
38
  self._num_dim = int(num_dim)
26
- self._num_arms = int(num_arms)
27
- self._num_metrics = int(num_metrics)
39
+ self._num_metrics = int(config.num_metrics)
28
40
  if self._num_metrics <= 0:
29
41
  raise ValueError(self._num_metrics)
30
-
31
- alpha = np.ones(self._num_metrics, dtype=float)
32
- self._weights = np.asarray(rng.dirichlet(alpha), dtype=float)
33
- self._alpha = 0.05
34
-
42
+ self._alpha = float(config.alpha)
43
+ self._rescalarize = config.rescalarize
44
+ self.incumbent_selector = ChebyshevIncumbentSelector(
45
+ num_metrics=self._num_metrics,
46
+ alpha=self._alpha,
47
+ noise_aware=config.noise_aware,
48
+ )
49
+ self.incumbent_selector.reset(rng)
50
+ self._weights = self.incumbent_selector.weights
35
51
  self._y_min: np.ndarray | Any | None = None
36
52
  self._y_max: np.ndarray | Any | None = None
53
+ self._incumbent_y_raw: np.ndarray | None = None
37
54
 
38
55
  @property
39
56
  def num_dim(self) -> int:
40
57
  return self._num_dim
41
58
 
42
- @property
43
- def num_arms(self) -> int:
44
- return self._num_arms
45
-
46
59
  @property
47
60
  def num_metrics(self) -> int:
48
61
  return self._num_metrics
@@ -55,68 +68,62 @@ class MorboTrustRegion:
55
68
  def length(self) -> float:
56
69
  return float(self._tr.length)
57
70
 
58
- def update(self, values: np.ndarray | Any) -> None:
59
- raise NotImplementedError(
60
- "Use update_xy(x_obs, y_obs) with multi-objective observations."
61
- )
71
+ @property
72
+ def rescalarize(self) -> Rescalarize:
73
+ return self._rescalarize
74
+
75
+ def resample_weights(self, rng: Generator) -> None:
76
+ self.incumbent_selector.reset(rng)
77
+ self._weights = self.incumbent_selector.weights
62
78
 
63
- def update_xy(
64
- self, x_obs: np.ndarray | Any, y_obs: np.ndarray | Any, *, k: Any = None
65
- ) -> None: # noqa: ARG002
79
+ def _update_ranges(self, y_obs):
80
+ self._y_min, self._y_max = y_obs.min(axis=0), y_obs.max(axis=0)
81
+
82
+ def update(self, y_obs: np.ndarray | Any, y_incumbent: np.ndarray | Any) -> None:
66
83
  import numpy as np
67
84
 
68
- x_obs = np.asarray(x_obs, dtype=float)
69
85
  y_obs = np.asarray(y_obs, dtype=float)
70
-
71
- if x_obs.ndim != 2 or x_obs.shape[1] != self._num_dim:
72
- raise ValueError(x_obs.shape)
73
- if y_obs.ndim != 2 or y_obs.shape[0] != x_obs.shape[0]:
74
- raise ValueError((x_obs.shape, y_obs.shape))
75
- if y_obs.shape[1] != self._num_metrics:
86
+ if y_obs.ndim != 2 or y_obs.shape[1] != self._num_metrics:
76
87
  raise ValueError((y_obs.shape, self._num_metrics))
77
-
78
- n = int(x_obs.shape[0])
88
+ n = int(y_obs.shape[0])
79
89
  if n == 0:
80
- self._y_min = None
81
- self._y_max = None
90
+ self._y_min, self._y_max = None, None
91
+ self._incumbent_y_raw = None
82
92
  self._tr.restart()
83
93
  return
84
-
85
94
  prev_n = int(self._tr.prev_num_obs)
86
95
  if n < prev_n:
87
96
  raise ValueError((n, prev_n))
88
-
89
- y_min_all = y_obs.min(axis=0)
90
- y_max_all = y_obs.max(axis=0)
91
- y_min_prev = y_obs[:prev_n].min(axis=0) if prev_n > 0 else y_min_all
92
- y_max_prev = y_obs[:prev_n].max(axis=0) if prev_n > 0 else y_max_all
93
-
94
- self._y_min = y_min_all
95
- self._y_max = y_max_all
96
-
97
+ self._y_min, self._y_max = y_obs.min(axis=0), y_obs.max(axis=0)
98
+ y_incumbent = np.asarray(y_incumbent, dtype=float).reshape(1, -1)
99
+ if y_incumbent.shape != (1, self._num_metrics):
100
+ raise ValueError(
101
+ f"y_incumbent must have shape (1, {self._num_metrics}), got {y_incumbent.shape}"
102
+ )
97
103
  if prev_n == 0:
98
- values = np.asarray(self.scalarize(y_obs, clip=True), dtype=float)
99
- if values.shape != (n,):
100
- raise RuntimeError((values.shape, n))
101
- self._tr.update(values)
104
+ self._handle_initial_update(y_incumbent, n)
102
105
  return
103
-
104
- if not np.isfinite(self._tr.best_value):
105
- raise RuntimeError(self._tr.best_value)
106
-
107
- values_old = self._scalarize_with_ranges(
108
- y_obs, y_min=y_min_prev, y_max=y_max_prev, clip=True
109
- )
110
- values_old = np.asarray(values_old, dtype=float)
111
- if values_old.shape != (n,):
112
- raise RuntimeError((values_old.shape, n))
113
-
114
- incumbent_old = float(np.max(values_old[:prev_n]))
115
- self._tr.best_value = incumbent_old
116
- if prev_n == n:
106
+ if self._incumbent_y_raw is None:
107
+ self._handle_initial_update(y_incumbent, n)
117
108
  return
109
+ scores = self.scalarize(
110
+ np.vstack([self._incumbent_y_raw, y_incumbent]), clip=True
111
+ )
112
+ old_score = float(scores[0])
113
+ new_score = float(scores[1])
114
+ self._tr.best_value = old_score
115
+ dummy_y_obs = np.zeros((n, 1))
116
+ self._tr.update(dummy_y_obs, np.array([new_score]))
117
+ if new_score > old_score:
118
+ self._incumbent_y_raw = y_incumbent.copy()
119
+
120
+ def _handle_initial_update(self, y_incumbent: np.ndarray, n: int) -> None:
121
+ import numpy as np
118
122
 
119
- self._tr.update(values_old)
123
+ self._incumbent_y_raw = y_incumbent.copy()
124
+ score = self.scalarize(y_incumbent, clip=True)
125
+ dummy_y_obs = np.zeros((n, 1))
126
+ self._tr.update(dummy_y_obs, score)
120
127
 
121
128
  def scalarize(self, y: np.ndarray | Any, *, clip: bool) -> np.ndarray:
122
129
  import numpy as np
@@ -126,7 +133,6 @@ class MorboTrustRegion:
126
133
  raise ValueError(y.shape)
127
134
  if self._y_min is None or self._y_max is None:
128
135
  raise RuntimeError("scalarize called before any observations")
129
-
130
136
  return self._scalarize_with_ranges(
131
137
  y, y_min=self._y_min, y_max=self._y_max, clip=clip
132
138
  )
@@ -148,7 +154,6 @@ class MorboTrustRegion:
148
154
  y_max = np.asarray(y_max, dtype=float).reshape(-1)
149
155
  if y_min.shape != (self._num_metrics,) or y_max.shape != (self._num_metrics,):
150
156
  raise ValueError((y_min.shape, y_max.shape, self._num_metrics))
151
-
152
157
  denom = y_max - y_min
153
158
  is_deg = denom <= 0.0
154
159
  denom_safe = np.where(is_deg, 1.0, denom)
@@ -163,10 +168,15 @@ class MorboTrustRegion:
163
168
  def needs_restart(self) -> bool:
164
169
  return self._tr.needs_restart()
165
170
 
166
- def restart(self) -> None:
171
+ def restart(self, rng: Generator | None = None) -> None:
172
+ from .config.rescalarize import Rescalarize
173
+
167
174
  self._y_min = None
168
175
  self._y_max = None
176
+ self._incumbent_y_raw = None
169
177
  self._tr.restart()
178
+ if rng is not None and self._rescalarize == Rescalarize.ON_RESTART:
179
+ self.resample_weights(rng)
170
180
 
171
181
  def validate_request(self, num_arms: int, *, is_fallback: bool = False) -> None:
172
182
  return self._tr.validate_request(num_arms, is_fallback=is_fallback)
@@ -183,7 +193,58 @@ class MorboTrustRegion:
183
193
  num_candidates: int,
184
194
  rng: Generator,
185
195
  sobol_engine: QMCEngine,
196
+ raasp_driver: RAASPDriver = RAASPDriver.ORIG,
197
+ num_pert: int = 20,
186
198
  ) -> np.ndarray:
187
- return self._tr.generate_candidates(
188
- x_center, lengthscales, num_candidates, rng, sobol_engine
199
+ from .tr_helpers import generate_tr_candidates
200
+
201
+ return generate_tr_candidates(
202
+ self._tr.compute_bounds_1d,
203
+ x_center,
204
+ lengthscales,
205
+ num_candidates,
206
+ rng=rng,
207
+ candidate_rv=self._candidate_rv,
208
+ sobol_engine=sobol_engine,
209
+ raasp_driver=raasp_driver,
210
+ num_pert=num_pert,
189
211
  )
212
+
213
+ def get_incumbent_indices(
214
+ self,
215
+ y: np.ndarray | Any,
216
+ rng: Generator,
217
+ ) -> np.ndarray:
218
+ import numpy as np
219
+
220
+ y = np.asarray(y, dtype=float)
221
+ if y.ndim != 2:
222
+ raise ValueError(y.shape)
223
+ n = y.shape[0]
224
+ if n == 0:
225
+ return np.array([], dtype=int)
226
+ from nds import ndomsort
227
+
228
+ idx_front = np.array(ndomsort.non_domin_sort(-y, only_front_indices=True))
229
+ return np.where(idx_front == 0)[0]
230
+
231
+ def get_incumbent_value(
232
+ self,
233
+ y_obs: np.ndarray | Any,
234
+ rng: Generator,
235
+ mu_obs: np.ndarray | None = None,
236
+ ) -> np.ndarray:
237
+ import numpy as np
238
+
239
+ y_obs = np.asarray(y_obs, dtype=float)
240
+ if y_obs.ndim != 2 or y_obs.shape[1] != self._num_metrics:
241
+ raise ValueError((y_obs.shape, self._num_metrics))
242
+ n = int(y_obs.shape[0])
243
+ if n == 0:
244
+ return np.array([], dtype=float)
245
+ idx = self.get_incumbent_index(y_obs, rng, mu=mu_obs)
246
+ use_mu = bool(getattr(self.incumbent_selector, "noise_aware", False))
247
+ values = np.asarray(mu_obs if use_mu else y_obs, dtype=float)
248
+ if values.ndim != 2 or values.shape[1] != self._num_metrics:
249
+ raise ValueError((values.shape, self._num_metrics))
250
+ return values[idx : idx + 1].copy()
@@ -1,65 +1,58 @@
1
1
  from __future__ import annotations
2
-
3
- from dataclasses import dataclass
2
+ from dataclasses import dataclass, field
4
3
  from typing import TYPE_CHECKING, Any
4
+ from .tr_helpers import ScalarIncumbentMixin
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  import numpy as np
8
- from numpy.random import Generator
9
- from scipy.stats._qmc import QMCEngine
8
+ from .components.incumbent_selector import IncumbentSelector
9
+ from .config.no_tr_config import NoTRConfig
10
10
 
11
11
 
12
12
  @dataclass
13
- class NoTrustRegion:
13
+ class NoTrustRegion(ScalarIncumbentMixin):
14
+ config: NoTRConfig
14
15
  num_dim: int
15
- num_arms: int
16
- length: float = 1.0
16
+ incumbent_selector: IncumbentSelector = field(default=None, repr=False)
17
+ length: float = field(default=1.0, init=False)
18
+
19
+ def __post_init__(self) -> None:
20
+ from .components.incumbent_selector import ScalarIncumbentSelector
21
+
22
+ if self.incumbent_selector is None:
23
+ self.incumbent_selector = ScalarIncumbentSelector(noise_aware=False)
24
+
25
+ @property
26
+ def num_metrics(self) -> int:
27
+ return 1
17
28
 
18
- def update(self, values: np.ndarray | Any) -> None:
29
+ def update(self, y_obs: np.ndarray | Any, y_incumbent: np.ndarray | Any) -> None:
19
30
  return
20
31
 
21
32
  def needs_restart(self) -> bool:
22
33
  return False
23
34
 
24
- def restart(self) -> None:
35
+ def restart(self, rng=None) -> None:
25
36
  return
26
37
 
27
38
  def validate_request(self, num_arms: int, *, is_fallback: bool = False) -> None:
28
- if is_fallback:
29
- if num_arms > self.num_arms:
30
- raise ValueError(
31
- f"num_arms {num_arms} > configured num_arms {self.num_arms}"
32
- )
33
- else:
34
- if num_arms != self.num_arms:
35
- raise ValueError(
36
- f"num_arms {num_arms} != configured num_arms {self.num_arms}"
37
- )
39
+ pass
38
40
 
39
41
  def compute_bounds_1d(
40
- self, x_center: np.ndarray | Any, lengthscales: np.ndarray | None = None
42
+ self,
43
+ x_center: np.ndarray | Any,
44
+ lengthscales: np.ndarray | None = None,
41
45
  ) -> tuple[np.ndarray, np.ndarray]:
42
- import numpy as np
46
+ from .tr_helpers import compute_full_box_bounds_1d
43
47
 
44
- lb = np.zeros_like(x_center, dtype=float)
45
- ub = np.ones_like(x_center, dtype=float)
46
- return lb, ub
48
+ return compute_full_box_bounds_1d(x_center)
47
49
 
48
- def generate_candidates(
50
+ def get_incumbent_indices(
49
51
  self,
50
- x_center: np.ndarray,
51
- lengthscales: np.ndarray | None,
52
- num_candidates: int,
53
- rng: Generator,
54
- sobol_engine: QMCEngine,
52
+ y: np.ndarray | Any,
53
+ rng,
54
+ mu: np.ndarray | None = None,
55
55
  ) -> np.ndarray:
56
- from .turbo_utils import generate_trust_region_candidates
56
+ import numpy as np
57
57
 
58
- return generate_trust_region_candidates(
59
- x_center,
60
- lengthscales,
61
- num_candidates,
62
- compute_bounds_1d=self.compute_bounds_1d,
63
- rng=rng,
64
- sobol_engine=sobol_engine,
65
- )
58
+ return np.array([self.get_incumbent_index(y, rng, mu=mu)])
enn/turbo/optimizer.py ADDED
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING
5
+
6
+ import numpy as np
7
+
8
+ from . import turbo_optimizer_utils, turbo_utils
9
+ from .components import AcquisitionOptimizer, Surrogate
10
+ from .config.enums import CandidateRV
11
+ from .strategies import OptimizationStrategy
12
+ from .types.appendable_array import AppendableArray
13
+
14
+ if TYPE_CHECKING:
15
+ from numpy.random import Generator
16
+
17
+ from .config.optimizer_config import OptimizerConfig
18
+
19
+
20
+ class Optimizer:
21
+ def __init__(
22
+ self,
23
+ *,
24
+ bounds: np.ndarray,
25
+ config: OptimizerConfig,
26
+ rng: Generator,
27
+ surrogate: Surrogate,
28
+ acquisition_optimizer: AcquisitionOptimizer,
29
+ strategy: OptimizationStrategy | None = None,
30
+ ) -> None:
31
+ self._config = config
32
+ bounds = np.asarray(bounds, dtype=float)
33
+ if bounds.ndim != 2 or bounds.shape[1] != 2:
34
+ raise ValueError(f"bounds must be (d, 2), got {bounds.shape}")
35
+ self._bounds = bounds
36
+ self._num_dim = bounds.shape[0]
37
+ self._rng = rng
38
+ self._surrogate = surrogate
39
+ self._acq_optimizer = acquisition_optimizer
40
+ self._strategy = (
41
+ strategy
42
+ if strategy is not None
43
+ else config.init.init_strategy.create_runtime_strategy(
44
+ bounds=self._bounds, rng=self._rng, num_init=config.init.num_init
45
+ )
46
+ )
47
+ self._tr_state = config.trust_region.build(
48
+ num_dim=self._num_dim,
49
+ rng=rng,
50
+ candidate_rv=config.candidate_rv,
51
+ )
52
+ self._trailing_obs = (
53
+ None if config.trailing_obs is None else int(config.trailing_obs)
54
+ )
55
+ self._gp_num_steps = 50
56
+ if self._trailing_obs is not None and self._trailing_obs <= 0:
57
+ raise ValueError(f"trailing_obs must be > 0, got {self._trailing_obs}")
58
+ self._x_obs = AppendableArray()
59
+ self._y_obs = AppendableArray()
60
+ self._yvar_obs = AppendableArray()
61
+ self._y_tr_list: list[float] | list[list[float]] = []
62
+ self._expects_yvar: bool | None = None
63
+ self._dt_fit = 0.0
64
+ self._dt_gen = 0.0
65
+ self._dt_sel = 0.0
66
+ self._dt_tell = 0.0
67
+ self._sobol_seed_base = int(rng.integers(2**31 - 1))
68
+ self._restart_generation = 0
69
+ self._incumbent_idx: int | None = None
70
+ self._incumbent_x_unit: np.ndarray | None = None
71
+ self._incumbent_y_scalar: np.ndarray | None = None
72
+
73
+ @property
74
+ def tr_obs_count(self) -> int:
75
+ return len(self._y_obs)
76
+
77
+ @property
78
+ def tr_length(self) -> float:
79
+ return float(self._tr_state.length)
80
+
81
+ def telemetry(self) -> turbo_utils.Telemetry:
82
+ return turbo_utils.Telemetry(
83
+ dt_fit=self._dt_fit,
84
+ dt_gen=self._dt_gen,
85
+ dt_sel=self._dt_sel,
86
+ dt_tell=self._dt_tell,
87
+ )
88
+
89
+ @property
90
+ def init_progress(self) -> tuple[int, int] | None:
91
+ return self._strategy.init_progress()
92
+
93
+ def ask(self, num_arms: int) -> np.ndarray:
94
+ num_arms = int(num_arms)
95
+ if num_arms <= 0:
96
+ raise ValueError(num_arms)
97
+ turbo_optimizer_utils.reset_timing(self)
98
+ return self._strategy.ask(self, num_arms)
99
+
100
+ def _ask_normal(self, num_arms: int, *, is_fallback: bool = False) -> np.ndarray:
101
+ self._tr_state.validate_request(num_arms, is_fallback=is_fallback)
102
+ self._maybe_resample_weights()
103
+ x_center = self._incumbent_x_unit
104
+ if x_center is None:
105
+ if len(self._y_obs) == 0:
106
+ raise RuntimeError("no observations")
107
+ x_center = np.full(self._num_dim, 0.5)
108
+ t0 = time.perf_counter()
109
+ lengthscales = self._surrogate.lengthscales
110
+ x_cand = self._generate_candidates(x_center, lengthscales, num_arms=num_arms)
111
+ self._dt_gen = time.perf_counter() - t0
112
+ t0 = time.perf_counter()
113
+ selected = self._acq_optimizer.select(
114
+ x_cand,
115
+ num_arms,
116
+ self._surrogate,
117
+ self._rng,
118
+ tr_state=self._tr_state,
119
+ )
120
+ self._dt_sel = time.perf_counter() - t0
121
+ return turbo_utils.from_unit(selected, self._bounds)
122
+
123
+ def _find_x_center(self, x_obs: np.ndarray, y_obs: np.ndarray) -> np.ndarray | None:
124
+ return self._incumbent_x_unit
125
+
126
+ def _maybe_resample_weights(self) -> None:
127
+ from .config.rescalarize import Rescalarize
128
+
129
+ if hasattr(self._tr_state, "rescalarize"):
130
+ if self._tr_state.rescalarize == Rescalarize.ON_PROPOSE:
131
+ self._tr_state.resample_weights(self._rng)
132
+
133
+ def _generate_candidates(
134
+ self,
135
+ x_center: np.ndarray,
136
+ lengthscales: np.ndarray | None,
137
+ *,
138
+ num_arms: int,
139
+ ) -> np.ndarray:
140
+ from . import tr_helpers
141
+
142
+ if lengthscales is not None:
143
+ lengthscales = np.asarray(lengthscales, dtype=float).reshape(-1)
144
+ if not np.all(np.isfinite(lengthscales)):
145
+ raise ValueError("lengthscales must be finite")
146
+ num_candidates = int(
147
+ self._config.candidates.num_candidates(
148
+ num_dim=self._num_dim, num_arms=num_arms
149
+ )
150
+ )
151
+ if num_candidates <= 0:
152
+ raise ValueError(num_candidates)
153
+ candidate_rv = self._config.candidate_rv
154
+ if candidate_rv == CandidateRV.SOBOL:
155
+ from scipy.stats import qmc
156
+
157
+ sobol_seed = turbo_optimizer_utils.sobol_seed_for_state(
158
+ self._sobol_seed_base,
159
+ restart_generation=self._restart_generation,
160
+ n_obs=len(self._x_obs),
161
+ num_arms=num_arms,
162
+ )
163
+ sobol_engine = qmc.Sobol(d=self._num_dim, scramble=True, seed=sobol_seed)
164
+ else:
165
+ sobol_engine = None
166
+ if getattr(self._tr_state, "uses_custom_candidate_gen", False):
167
+ return self._tr_state.generate_candidates(
168
+ x_center,
169
+ lengthscales,
170
+ num_candidates,
171
+ rng=self._rng,
172
+ sobol_engine=sobol_engine,
173
+ raasp_driver=self._config.raasp_driver,
174
+ num_pert=20,
175
+ )
176
+ return tr_helpers.generate_tr_candidates(
177
+ self._tr_state.compute_bounds_1d,
178
+ x_center,
179
+ lengthscales,
180
+ num_candidates,
181
+ rng=self._rng,
182
+ candidate_rv=candidate_rv,
183
+ sobol_engine=sobol_engine,
184
+ raasp_driver=self._config.raasp_driver,
185
+ num_pert=20,
186
+ )
187
+
188
+ def _validate_tell_inputs(
189
+ self, x: np.ndarray, y: np.ndarray, y_var: np.ndarray | None
190
+ ) -> turbo_optimizer_utils.TellInputs:
191
+ inputs = turbo_optimizer_utils.validate_tell_inputs(x, y, y_var, self._num_dim)
192
+ tr_num_metrics = getattr(self._tr_state, "num_metrics", 1)
193
+ if inputs.num_metrics != tr_num_metrics:
194
+ raise ValueError(
195
+ f"y has {inputs.num_metrics} metrics but trust region expects {tr_num_metrics}"
196
+ )
197
+ if self._expects_yvar is None:
198
+ self._expects_yvar = inputs.y_var is not None
199
+ if (inputs.y_var is not None) != bool(self._expects_yvar):
200
+ raise ValueError(
201
+ f"y_var must be {'provided' if self._expects_yvar else 'omitted'} on every tell()"
202
+ )
203
+ return inputs
204
+
205
+ def _update_incumbent(self) -> None:
206
+ if len(self._y_obs) == 0:
207
+ self._incumbent_idx = None
208
+ self._incumbent_x_unit = None
209
+ self._incumbent_y_scalar = None
210
+ return
211
+ x_obs = self._x_obs.view()
212
+ y_obs = self._y_obs.view()
213
+ candidate_indices = self._surrogate.get_incumbent_candidate_indices(y_obs)
214
+ x_cand = x_obs[candidate_indices]
215
+ y_cand = y_obs[candidate_indices]
216
+ mu_cand = None
217
+ noise_aware = False
218
+ if hasattr(self._tr_state, "incumbent_selector"):
219
+ noise_aware = getattr(
220
+ self._tr_state.incumbent_selector, "noise_aware", False
221
+ )
222
+ elif hasattr(self._tr_state, "config"):
223
+ noise_aware = getattr(self._tr_state.config, "noise_aware", False)
224
+ if noise_aware:
225
+ try:
226
+ mu_cand = self._surrogate.predict(x_cand).mu
227
+ except RuntimeError:
228
+ mu_cand = None
229
+ idx_in_cand = self._tr_state.get_incumbent_index(y_cand, self._rng, mu=mu_cand)
230
+ self._incumbent_idx = int(candidate_indices[idx_in_cand])
231
+ self._incumbent_x_unit = x_obs[self._incumbent_idx]
232
+ if noise_aware and mu_cand is not None:
233
+ self._incumbent_y_scalar = mu_cand[idx_in_cand : idx_in_cand + 1].copy()
234
+ else:
235
+ self._incumbent_y_scalar = y_cand[idx_in_cand : idx_in_cand + 1].copy()
236
+
237
+ def _trim_trailing_obs(self) -> None:
238
+ incumbent_indices = np.array([self._incumbent_idx], dtype=int)
239
+ obs = turbo_optimizer_utils.trim_trailing_observations(
240
+ self._x_obs.view().tolist(),
241
+ self._y_obs.view().tolist(),
242
+ self._y_tr_list,
243
+ self._yvar_obs.view().tolist() if len(self._yvar_obs) > 0 else [],
244
+ trailing_obs=self._trailing_obs,
245
+ incumbent_indices=incumbent_indices,
246
+ )
247
+ self._x_obs = AppendableArray()
248
+ for x in obs.x_obs:
249
+ self._x_obs.append(np.array(x))
250
+ self._y_obs = AppendableArray()
251
+ for y in obs.y_obs:
252
+ self._y_obs.append(np.array(y))
253
+ self._yvar_obs = AppendableArray()
254
+ if obs.yvar_obs:
255
+ for yvar in obs.yvar_obs:
256
+ self._yvar_obs.append(np.array(yvar))
257
+ self._y_tr_list = obs.y_tr
258
+
259
+ def _update_best_value_if_needed(self) -> None:
260
+ pass
261
+
262
+ def tell(
263
+ self, x: np.ndarray, y: np.ndarray, y_var: np.ndarray | None = None
264
+ ) -> np.ndarray:
265
+ with turbo_utils.record_duration(
266
+ lambda dt: setattr(self, "_dt_tell", float(dt))
267
+ ):
268
+ inputs = self._validate_tell_inputs(x, y, y_var)
269
+ if inputs.x.shape[0] == 0:
270
+ return (
271
+ np.array([], dtype=float)
272
+ if inputs.num_metrics == 1
273
+ else np.empty((0, inputs.num_metrics), dtype=float)
274
+ )
275
+ x_unit = turbo_utils.to_unit(inputs.x, self._bounds)
276
+ for i in range(inputs.x.shape[0]):
277
+ self._x_obs.append(x_unit[i])
278
+ self._y_obs.append(inputs.y[i])
279
+ if inputs.y_var is not None:
280
+ self._yvar_obs.append(inputs.y_var[i])
281
+ return self._strategy.tell(self, inputs, x_unit=x_unit)
282
+
283
+
284
+ def create_optimizer(
285
+ *,
286
+ bounds: np.ndarray,
287
+ config: OptimizerConfig,
288
+ rng: Generator,
289
+ ) -> Optimizer:
290
+ from .components.builder import build_acquisition_optimizer, build_surrogate
291
+
292
+ surrogate = build_surrogate(config)
293
+ acq_optimizer = build_acquisition_optimizer(config)
294
+ return Optimizer(
295
+ bounds=bounds,
296
+ config=config,
297
+ rng=rng,
298
+ surrogate=surrogate,
299
+ acquisition_optimizer=acq_optimizer,
300
+ )
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+ from . import config as _config
3
+
4
+ __all__ = list(_config.__all__)
5
+
6
+
7
+ def __getattr__(name: str) -> object:
8
+ return getattr(_config, name)