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.
- enn/__init__.py +25 -13
- enn/benchmarks/__init__.py +3 -0
- enn/benchmarks/ackley.py +5 -0
- enn/benchmarks/ackley_class.py +17 -0
- enn/benchmarks/ackley_core.py +12 -0
- enn/benchmarks/double_ackley.py +24 -0
- enn/enn/candidates.py +14 -0
- enn/enn/conditional_posterior_draw_internals.py +15 -0
- enn/enn/draw_internals.py +15 -0
- enn/enn/enn.py +16 -269
- enn/enn/enn_class.py +423 -0
- enn/enn/enn_conditional.py +325 -0
- enn/enn/enn_fit.py +69 -70
- enn/enn/enn_hash.py +79 -0
- enn/enn/enn_index.py +92 -0
- enn/enn/enn_like_protocol.py +35 -0
- enn/enn/enn_normal.py +0 -1
- enn/enn/enn_params.py +3 -22
- enn/enn/enn_params_class.py +24 -0
- enn/enn/enn_util.py +60 -46
- enn/enn/neighbor_data.py +14 -0
- enn/enn/neighbors.py +14 -0
- enn/enn/posterior_flags.py +8 -0
- enn/enn/weighted_stats.py +14 -0
- enn/turbo/components/__init__.py +41 -0
- enn/turbo/components/acquisition.py +13 -0
- enn/turbo/components/acquisition_optimizer_protocol.py +19 -0
- enn/turbo/components/builder.py +22 -0
- enn/turbo/components/chebyshev_incumbent_selector.py +76 -0
- enn/turbo/components/enn_surrogate.py +115 -0
- enn/turbo/components/gp_surrogate.py +144 -0
- enn/turbo/components/hnr_acq_optimizer.py +83 -0
- enn/turbo/components/incumbent_selector.py +11 -0
- enn/turbo/components/incumbent_selector_protocol.py +16 -0
- enn/turbo/components/no_incumbent_selector.py +21 -0
- enn/turbo/components/no_surrogate.py +49 -0
- enn/turbo/components/pareto_acq_optimizer.py +49 -0
- enn/turbo/components/posterior_result.py +12 -0
- enn/turbo/components/protocols.py +13 -0
- enn/turbo/components/random_acq_optimizer.py +21 -0
- enn/turbo/components/scalar_incumbent_selector.py +39 -0
- enn/turbo/components/surrogate_protocol.py +32 -0
- enn/turbo/components/surrogate_result.py +12 -0
- enn/turbo/components/surrogates.py +5 -0
- enn/turbo/components/thompson_acq_optimizer.py +49 -0
- enn/turbo/components/trust_region_protocol.py +24 -0
- enn/turbo/components/ucb_acq_optimizer.py +49 -0
- enn/turbo/config/__init__.py +87 -0
- enn/turbo/config/acq_type.py +8 -0
- enn/turbo/config/acquisition.py +26 -0
- enn/turbo/config/base.py +4 -0
- enn/turbo/config/candidate_gen_config.py +49 -0
- enn/turbo/config/candidate_rv.py +7 -0
- enn/turbo/config/draw_acquisition_config.py +14 -0
- enn/turbo/config/enn_index_driver.py +6 -0
- enn/turbo/config/enn_surrogate_config.py +42 -0
- enn/turbo/config/enums.py +7 -0
- enn/turbo/config/factory.py +118 -0
- enn/turbo/config/gp_surrogate_config.py +14 -0
- enn/turbo/config/hnr_optimizer_config.py +7 -0
- enn/turbo/config/init_config.py +17 -0
- enn/turbo/config/init_strategies/__init__.py +9 -0
- enn/turbo/config/init_strategies/hybrid_init.py +23 -0
- enn/turbo/config/init_strategies/init_strategy.py +19 -0
- enn/turbo/config/init_strategies/lhd_only_init.py +24 -0
- enn/turbo/config/morbo_tr_config.py +82 -0
- enn/turbo/config/nds_optimizer_config.py +7 -0
- enn/turbo/config/no_surrogate_config.py +14 -0
- enn/turbo/config/no_tr_config.py +31 -0
- enn/turbo/config/optimizer_config.py +72 -0
- enn/turbo/config/pareto_acquisition_config.py +14 -0
- enn/turbo/config/raasp_driver.py +6 -0
- enn/turbo/config/raasp_optimizer_config.py +7 -0
- enn/turbo/config/random_acquisition_config.py +14 -0
- enn/turbo/config/rescalarize.py +7 -0
- enn/turbo/config/surrogate.py +12 -0
- enn/turbo/config/trust_region.py +34 -0
- enn/turbo/config/turbo_tr_config.py +71 -0
- enn/turbo/config/ucb_acquisition_config.py +14 -0
- enn/turbo/config/validation.py +45 -0
- enn/turbo/hypervolume.py +30 -0
- enn/turbo/impl_helpers.py +68 -0
- enn/turbo/morbo_trust_region.py +131 -70
- enn/turbo/no_trust_region.py +32 -39
- enn/turbo/optimizer.py +300 -0
- enn/turbo/optimizer_config.py +8 -0
- enn/turbo/proposal.py +36 -38
- enn/turbo/sampling.py +21 -0
- enn/turbo/strategies/__init__.py +9 -0
- enn/turbo/strategies/lhd_only_strategy.py +36 -0
- enn/turbo/strategies/optimization_strategy.py +19 -0
- enn/turbo/strategies/turbo_hybrid_strategy.py +124 -0
- enn/turbo/tr_helpers.py +202 -0
- enn/turbo/turbo_gp.py +0 -1
- enn/turbo/turbo_gp_base.py +0 -1
- enn/turbo/turbo_gp_fit.py +187 -0
- enn/turbo/turbo_gp_noisy.py +0 -1
- enn/turbo/turbo_optimizer_utils.py +98 -0
- enn/turbo/turbo_trust_region.py +126 -58
- enn/turbo/turbo_utils.py +98 -161
- enn/turbo/types/__init__.py +7 -0
- enn/turbo/types/appendable_array.py +85 -0
- enn/turbo/types/gp_data_prep.py +13 -0
- enn/turbo/types/gp_fit_result.py +11 -0
- enn/turbo/types/obs_lists.py +10 -0
- enn/turbo/types/prepare_ask_result.py +14 -0
- enn/turbo/types/tell_inputs.py +14 -0
- {ennbo-0.1.2.dist-info → ennbo-0.1.7.dist-info}/METADATA +18 -11
- ennbo-0.1.7.dist-info/RECORD +111 -0
- enn/enn/__init__.py +0 -4
- enn/turbo/__init__.py +0 -11
- enn/turbo/base_turbo_impl.py +0 -144
- enn/turbo/lhd_only_impl.py +0 -49
- enn/turbo/turbo_config.py +0 -72
- enn/turbo/turbo_enn_impl.py +0 -201
- enn/turbo/turbo_mode.py +0 -10
- enn/turbo/turbo_mode_impl.py +0 -76
- enn/turbo/turbo_one_impl.py +0 -302
- enn/turbo/turbo_optimizer.py +0 -525
- enn/turbo/turbo_zero_impl.py +0 -29
- ennbo-0.1.2.dist-info/RECORD +0 -29
- {ennbo-0.1.2.dist-info → ennbo-0.1.7.dist-info}/WHEEL +0 -0
- {ennbo-0.1.2.dist-info → ennbo-0.1.7.dist-info}/licenses/LICENSE +0 -0
enn/turbo/morbo_trust_region.py
CHANGED
|
@@ -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 .
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
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
|
-
|
|
32
|
-
self.
|
|
33
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
64
|
-
self,
|
|
65
|
-
|
|
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.
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
188
|
-
|
|
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()
|
enn/turbo/no_trust_region.py
CHANGED
|
@@ -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
|
|
9
|
-
from
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
42
|
+
self,
|
|
43
|
+
x_center: np.ndarray | Any,
|
|
44
|
+
lengthscales: np.ndarray | None = None,
|
|
41
45
|
) -> tuple[np.ndarray, np.ndarray]:
|
|
42
|
-
|
|
46
|
+
from .tr_helpers import compute_full_box_bounds_1d
|
|
43
47
|
|
|
44
|
-
|
|
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
|
|
50
|
+
def get_incumbent_indices(
|
|
49
51
|
self,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
+
import numpy as np
|
|
57
57
|
|
|
58
|
-
return
|
|
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
|
+
)
|