ennbo 0.1.0__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 +24 -0
- enn/enn/__init__.py +4 -0
- enn/enn/enn.py +229 -0
- enn/enn/enn_fit.py +143 -0
- enn/enn/enn_normal.py +27 -0
- enn/enn/enn_params.py +10 -0
- enn/enn/enn_util.py +96 -0
- enn/turbo/__init__.py +11 -0
- enn/turbo/base_turbo_impl.py +98 -0
- enn/turbo/lhd_only_impl.py +42 -0
- enn/turbo/proposal.py +133 -0
- enn/turbo/turbo_config.py +28 -0
- enn/turbo/turbo_enn_impl.py +176 -0
- enn/turbo/turbo_gp.py +29 -0
- enn/turbo/turbo_gp_base.py +27 -0
- enn/turbo/turbo_gp_noisy.py +36 -0
- enn/turbo/turbo_mode.py +10 -0
- enn/turbo/turbo_mode_impl.py +67 -0
- enn/turbo/turbo_one_impl.py +163 -0
- enn/turbo/turbo_optimizer.py +337 -0
- enn/turbo/turbo_trust_region.py +123 -0
- enn/turbo/turbo_utils.py +248 -0
- enn/turbo/turbo_zero_impl.py +24 -0
- ennbo-0.1.0.dist-info/METADATA +109 -0
- ennbo-0.1.0.dist-info/RECORD +27 -0
- ennbo-0.1.0.dist-info/WHEEL +4 -0
- ennbo-0.1.0.dist-info/licenses/LICENSE +21 -0
enn/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .enn import EpistemicNearestNeighbors, enn_fit
|
|
4
|
+
|
|
5
|
+
_LAZY_IMPORTS = ("TurboMode", "TurboOptimizer", "Turbo", "Telemetry")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _lazy_load(name: str):
|
|
9
|
+
from . import turbo
|
|
10
|
+
|
|
11
|
+
return getattr(turbo, name)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __getattr__(name: str):
|
|
15
|
+
if name in _LAZY_IMPORTS:
|
|
16
|
+
return _lazy_load(name)
|
|
17
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__: list[str] = [
|
|
21
|
+
"EpistemicNearestNeighbors",
|
|
22
|
+
"enn_fit",
|
|
23
|
+
*_LAZY_IMPORTS,
|
|
24
|
+
]
|
enn/enn/__init__.py
ADDED
enn/enn/enn.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .enn_normal import ENNNormal
|
|
9
|
+
from .enn_params import ENNParams
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EpistemicNearestNeighbors:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
train_x: np.ndarray | Any,
|
|
16
|
+
train_y: np.ndarray | Any,
|
|
17
|
+
train_yvar: np.ndarray | Any | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
if train_x.ndim != 2:
|
|
22
|
+
raise ValueError(train_x.shape)
|
|
23
|
+
if train_y.ndim != 2:
|
|
24
|
+
raise ValueError(train_y.shape)
|
|
25
|
+
if train_x.shape[0] != train_y.shape[0]:
|
|
26
|
+
raise ValueError((train_x.shape, train_y.shape))
|
|
27
|
+
if train_yvar is not None:
|
|
28
|
+
if train_yvar.ndim != 2:
|
|
29
|
+
raise ValueError(train_yvar.shape)
|
|
30
|
+
if train_y.shape != train_yvar.shape:
|
|
31
|
+
raise ValueError((train_y.shape, train_yvar.shape))
|
|
32
|
+
self._train_x = np.asarray(train_x, dtype=float)
|
|
33
|
+
self._train_y = np.asarray(train_y, dtype=float)
|
|
34
|
+
self._train_yvar = (
|
|
35
|
+
np.asarray(train_yvar, dtype=float) if train_yvar is not None else None
|
|
36
|
+
)
|
|
37
|
+
self._num_obs, self._num_dim = self._train_x.shape
|
|
38
|
+
_, self._num_metrics = self._train_y.shape
|
|
39
|
+
self._eps_var = 1e-9
|
|
40
|
+
if len(self._train_y) < 2:
|
|
41
|
+
self._y_scale = np.ones(shape=(1, self._num_metrics), dtype=float)
|
|
42
|
+
else:
|
|
43
|
+
self._y_scale = np.std(self._train_y, axis=0, keepdims=True).astype(float)
|
|
44
|
+
|
|
45
|
+
self._index: Any | None = None
|
|
46
|
+
self._build_index()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def train_x(self) -> np.ndarray:
|
|
50
|
+
return self._train_x
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def train_y(self) -> np.ndarray:
|
|
54
|
+
return self._train_y
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def train_yvar(self) -> np.ndarray | None:
|
|
58
|
+
return self._train_yvar
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def num_outputs(self) -> int:
|
|
62
|
+
return self._num_metrics
|
|
63
|
+
|
|
64
|
+
def __len__(self) -> int:
|
|
65
|
+
return self._num_obs
|
|
66
|
+
|
|
67
|
+
def _build_index(self) -> None:
|
|
68
|
+
import faiss
|
|
69
|
+
import numpy as np
|
|
70
|
+
|
|
71
|
+
if self._num_obs == 0:
|
|
72
|
+
return
|
|
73
|
+
x_f32 = self._train_x.astype(np.float32, copy=False)
|
|
74
|
+
index = faiss.IndexFlatL2(self._num_dim)
|
|
75
|
+
index.add(x_f32)
|
|
76
|
+
self._index = index
|
|
77
|
+
|
|
78
|
+
def posterior(
|
|
79
|
+
self,
|
|
80
|
+
x: np.ndarray | Any,
|
|
81
|
+
*,
|
|
82
|
+
params: ENNParams,
|
|
83
|
+
exclude_nearest: bool = False,
|
|
84
|
+
observation_noise: bool = False,
|
|
85
|
+
) -> ENNNormal:
|
|
86
|
+
from .enn_normal import ENNNormal
|
|
87
|
+
|
|
88
|
+
post_batch = self.batch_posterior(
|
|
89
|
+
x,
|
|
90
|
+
[params],
|
|
91
|
+
exclude_nearest=exclude_nearest,
|
|
92
|
+
observation_noise=observation_noise,
|
|
93
|
+
)
|
|
94
|
+
mu = post_batch.mu[0]
|
|
95
|
+
se = post_batch.se[0]
|
|
96
|
+
return ENNNormal(mu, se)
|
|
97
|
+
|
|
98
|
+
def batch_posterior(
|
|
99
|
+
self,
|
|
100
|
+
x: np.ndarray | Any,
|
|
101
|
+
paramss: list[ENNParams],
|
|
102
|
+
*,
|
|
103
|
+
exclude_nearest: bool = False,
|
|
104
|
+
observation_noise: bool = False,
|
|
105
|
+
) -> ENNNormal:
|
|
106
|
+
import numpy as np
|
|
107
|
+
|
|
108
|
+
from .enn_normal import ENNNormal
|
|
109
|
+
|
|
110
|
+
if x.ndim != 2:
|
|
111
|
+
raise ValueError(x.shape)
|
|
112
|
+
if x.shape[1] != self._num_dim:
|
|
113
|
+
raise ValueError(x.shape)
|
|
114
|
+
if len(paramss) == 0:
|
|
115
|
+
raise ValueError("paramss must be non-empty")
|
|
116
|
+
batch_size = x.shape[0]
|
|
117
|
+
num_params = len(paramss)
|
|
118
|
+
if len(self) == 0:
|
|
119
|
+
mu = np.zeros((num_params, batch_size, self._num_metrics), dtype=float)
|
|
120
|
+
se = np.ones((num_params, batch_size, self._num_metrics), dtype=float)
|
|
121
|
+
return ENNNormal(mu, se)
|
|
122
|
+
max_k = max(params.k for params in paramss)
|
|
123
|
+
if exclude_nearest:
|
|
124
|
+
if len(self) <= 1:
|
|
125
|
+
raise ValueError(len(self))
|
|
126
|
+
search_k = int(min(max_k + 1, len(self)))
|
|
127
|
+
else:
|
|
128
|
+
search_k = int(min(max_k, len(self)))
|
|
129
|
+
x_f32 = x.astype(np.float32, copy=False)
|
|
130
|
+
if self._index is None:
|
|
131
|
+
raise RuntimeError("index is not initialized")
|
|
132
|
+
dist2s_full, idx_full = self._index.search(x_f32, search_k)
|
|
133
|
+
dist2s_full = dist2s_full.astype(float)
|
|
134
|
+
idx_full = idx_full.astype(int)
|
|
135
|
+
if exclude_nearest:
|
|
136
|
+
dist2s_full = dist2s_full[:, 1:]
|
|
137
|
+
idx_full = idx_full[:, 1:]
|
|
138
|
+
mu_all = np.zeros((num_params, batch_size, self._num_metrics), dtype=float)
|
|
139
|
+
se_all = np.zeros((num_params, batch_size, self._num_metrics), dtype=float)
|
|
140
|
+
available_k = search_k - 1 if exclude_nearest else search_k
|
|
141
|
+
for i, params in enumerate(paramss):
|
|
142
|
+
k = min(params.k, available_k)
|
|
143
|
+
if k > dist2s_full.shape[1]:
|
|
144
|
+
raise RuntimeError(
|
|
145
|
+
f"k={k} exceeds available columns={dist2s_full.shape[1]}"
|
|
146
|
+
)
|
|
147
|
+
if k == 0:
|
|
148
|
+
mu_all[i] = np.zeros((batch_size, self._num_metrics), dtype=float)
|
|
149
|
+
se_all[i] = np.ones((batch_size, self._num_metrics), dtype=float)
|
|
150
|
+
continue
|
|
151
|
+
dist2s = dist2s_full[:, :k]
|
|
152
|
+
idx = idx_full[:, :k]
|
|
153
|
+
y_neighbors = self._train_y[idx]
|
|
154
|
+
|
|
155
|
+
dist2s_expanded = dist2s[..., np.newaxis]
|
|
156
|
+
var_component = (
|
|
157
|
+
params.ale_homoscedastic_scale + params.epi_var_scale * dist2s_expanded
|
|
158
|
+
)
|
|
159
|
+
if self._train_yvar is not None:
|
|
160
|
+
yvar_neighbors = self._train_yvar[idx] / self._y_scale**2
|
|
161
|
+
var_component = var_component + yvar_neighbors
|
|
162
|
+
else:
|
|
163
|
+
yvar_neighbors = None
|
|
164
|
+
|
|
165
|
+
w = 1.0 / (self._eps_var + var_component)
|
|
166
|
+
norm = np.sum(w, axis=1)
|
|
167
|
+
mu_all[i] = np.sum(w * y_neighbors, axis=1) / norm
|
|
168
|
+
epistemic_var = 1.0 / norm
|
|
169
|
+
vvar = epistemic_var
|
|
170
|
+
if observation_noise:
|
|
171
|
+
vvar = vvar + params.ale_homoscedastic_scale
|
|
172
|
+
if yvar_neighbors is not None:
|
|
173
|
+
ale_heteroscedastic = np.sum(w * yvar_neighbors, axis=1) / norm
|
|
174
|
+
vvar = vvar + ale_heteroscedastic
|
|
175
|
+
vvar = np.maximum(vvar, self._eps_var)
|
|
176
|
+
se_all[i] = np.sqrt(vvar) * self._y_scale
|
|
177
|
+
return ENNNormal(mu_all, se_all)
|
|
178
|
+
|
|
179
|
+
def neighbors(
|
|
180
|
+
self,
|
|
181
|
+
x: np.ndarray | Any,
|
|
182
|
+
k: int,
|
|
183
|
+
*,
|
|
184
|
+
exclude_nearest: bool = False,
|
|
185
|
+
) -> list[tuple[np.ndarray, np.ndarray]]:
|
|
186
|
+
import numpy as np
|
|
187
|
+
|
|
188
|
+
x = np.asarray(x, dtype=float)
|
|
189
|
+
if x.ndim == 1:
|
|
190
|
+
x = x[np.newaxis, :]
|
|
191
|
+
if x.ndim != 2:
|
|
192
|
+
raise ValueError(f"x must be 1D or 2D, got shape {x.shape}")
|
|
193
|
+
if x.shape[0] != 1:
|
|
194
|
+
raise ValueError(f"x must be a single point, got shape {x.shape}")
|
|
195
|
+
if x.shape[1] != self._num_dim:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"x must have {self._num_dim} dimensions, got {x.shape[1]}"
|
|
198
|
+
)
|
|
199
|
+
if k < 0:
|
|
200
|
+
raise ValueError(f"k must be non-negative, got {k}")
|
|
201
|
+
if len(self) == 0:
|
|
202
|
+
return []
|
|
203
|
+
if exclude_nearest:
|
|
204
|
+
if len(self) <= 1:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"exclude_nearest=True requires at least 2 observations, got {len(self)}"
|
|
207
|
+
)
|
|
208
|
+
search_k = int(min(k + 1, len(self)))
|
|
209
|
+
else:
|
|
210
|
+
search_k = int(min(k, len(self)))
|
|
211
|
+
if search_k == 0:
|
|
212
|
+
return []
|
|
213
|
+
x_f32 = x.astype(np.float32, copy=False)
|
|
214
|
+
if self._index is None:
|
|
215
|
+
raise RuntimeError("index is not initialized")
|
|
216
|
+
dist2s_full, idx_full = self._index.search(x_f32, search_k)
|
|
217
|
+
dist2s_full = dist2s_full.astype(float)
|
|
218
|
+
idx_full = idx_full.astype(int)
|
|
219
|
+
if exclude_nearest:
|
|
220
|
+
dist2s_full = dist2s_full[:, 1:]
|
|
221
|
+
idx_full = idx_full[:, 1:]
|
|
222
|
+
actual_k = min(k, len(idx_full[0]))
|
|
223
|
+
idx = idx_full[0, :actual_k]
|
|
224
|
+
result = []
|
|
225
|
+
for i in idx:
|
|
226
|
+
x_neighbor = self._train_x[i].copy()
|
|
227
|
+
y_neighbor = self._train_y[i].copy()
|
|
228
|
+
result.append((x_neighbor, y_neighbor))
|
|
229
|
+
return result
|
enn/enn/enn_fit.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.random import Generator
|
|
8
|
+
|
|
9
|
+
from .enn import EpistemicNearestNeighbors
|
|
10
|
+
from .enn_params import ENNParams
|
|
11
|
+
|
|
12
|
+
from .enn_util import standardize_y
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def subsample_loglik(
|
|
16
|
+
model: EpistemicNearestNeighbors | Any,
|
|
17
|
+
x: np.ndarray | Any,
|
|
18
|
+
y: np.ndarray | Any,
|
|
19
|
+
*,
|
|
20
|
+
paramss: list[ENNParams] | list[Any],
|
|
21
|
+
P: int = 10,
|
|
22
|
+
rng: Generator | Any,
|
|
23
|
+
) -> list[float]:
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
if x.ndim != 2:
|
|
27
|
+
raise ValueError(x.shape)
|
|
28
|
+
if y.ndim != 1:
|
|
29
|
+
raise ValueError(y.shape)
|
|
30
|
+
if x.shape[0] != y.shape[0]:
|
|
31
|
+
raise ValueError((x.shape, y.shape))
|
|
32
|
+
if P <= 0:
|
|
33
|
+
raise ValueError(P)
|
|
34
|
+
if len(paramss) == 0:
|
|
35
|
+
raise ValueError("paramss must be non-empty")
|
|
36
|
+
n = x.shape[0]
|
|
37
|
+
if n == 0:
|
|
38
|
+
return [0.0] * len(paramss)
|
|
39
|
+
if len(model) <= 1:
|
|
40
|
+
return [0.0] * len(paramss)
|
|
41
|
+
P_actual = min(P, n)
|
|
42
|
+
if P_actual == n:
|
|
43
|
+
indices = np.arange(n, dtype=int)
|
|
44
|
+
else:
|
|
45
|
+
indices = rng.permutation(n)[:P_actual]
|
|
46
|
+
x_selected = x[indices]
|
|
47
|
+
y_selected = y[indices]
|
|
48
|
+
if not np.isfinite(y_selected).all():
|
|
49
|
+
return [0.0] * len(paramss)
|
|
50
|
+
post_batch = model.batch_posterior(
|
|
51
|
+
x_selected, paramss, exclude_nearest=True, observation_noise=True
|
|
52
|
+
)
|
|
53
|
+
mu_batch = post_batch.mu
|
|
54
|
+
se_batch = post_batch.se
|
|
55
|
+
if mu_batch.shape[2] == 1:
|
|
56
|
+
mu_batch = mu_batch[:, :, 0]
|
|
57
|
+
se_batch = se_batch[:, :, 0]
|
|
58
|
+
num_params = len(paramss)
|
|
59
|
+
if mu_batch.shape != (num_params, P_actual) or se_batch.shape != (
|
|
60
|
+
num_params,
|
|
61
|
+
P_actual,
|
|
62
|
+
):
|
|
63
|
+
raise ValueError((mu_batch.shape, se_batch.shape, (num_params, P_actual)))
|
|
64
|
+
_, y_std = standardize_y(y)
|
|
65
|
+
y_scaled = y_selected / y_std
|
|
66
|
+
mu_scaled = mu_batch / y_std
|
|
67
|
+
se_scaled = se_batch / y_std
|
|
68
|
+
result = []
|
|
69
|
+
for i in range(num_params):
|
|
70
|
+
mu_i = mu_scaled[i]
|
|
71
|
+
se_i = se_scaled[i]
|
|
72
|
+
if not np.isfinite(mu_i).all() or not np.isfinite(se_i).all():
|
|
73
|
+
result.append(0.0)
|
|
74
|
+
continue
|
|
75
|
+
if np.any(se_i <= 0.0):
|
|
76
|
+
result.append(0.0)
|
|
77
|
+
continue
|
|
78
|
+
diff = y_scaled - mu_i
|
|
79
|
+
var_scaled = se_i**2
|
|
80
|
+
log_term = np.log(2.0 * np.pi * var_scaled)
|
|
81
|
+
quad = diff**2 / var_scaled
|
|
82
|
+
loglik = -0.5 * np.sum(log_term + quad)
|
|
83
|
+
if not np.isfinite(loglik):
|
|
84
|
+
result.append(0.0)
|
|
85
|
+
continue
|
|
86
|
+
result.append(float(loglik))
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def enn_fit(
|
|
91
|
+
model: EpistemicNearestNeighbors | Any,
|
|
92
|
+
*,
|
|
93
|
+
k: int,
|
|
94
|
+
num_fit_candidates: int,
|
|
95
|
+
num_fit_samples: int = 10,
|
|
96
|
+
rng: Generator | Any,
|
|
97
|
+
params_warm_start: ENNParams | Any | None = None,
|
|
98
|
+
) -> ENNParams:
|
|
99
|
+
from .enn_params import ENNParams
|
|
100
|
+
|
|
101
|
+
train_x = model.train_x
|
|
102
|
+
train_y = model.train_y
|
|
103
|
+
train_yvar = model.train_yvar
|
|
104
|
+
if train_y.shape[1] != 1:
|
|
105
|
+
raise ValueError(train_y.shape)
|
|
106
|
+
if train_yvar is not None and train_yvar.shape[1] != 1:
|
|
107
|
+
raise ValueError(train_yvar.shape)
|
|
108
|
+
y = train_y[:, 0]
|
|
109
|
+
log_min = -3.0
|
|
110
|
+
log_max = 3.0
|
|
111
|
+
epi_var_scale_log_values = rng.uniform(log_min, log_max, size=num_fit_candidates)
|
|
112
|
+
epi_var_scale_values = 10**epi_var_scale_log_values
|
|
113
|
+
ale_homoscedastic_log_values = rng.uniform(
|
|
114
|
+
log_min, log_max, size=num_fit_candidates
|
|
115
|
+
)
|
|
116
|
+
ale_homoscedastic_values = 10**ale_homoscedastic_log_values
|
|
117
|
+
paramss = [
|
|
118
|
+
ENNParams(
|
|
119
|
+
k=k,
|
|
120
|
+
epi_var_scale=float(epi_val),
|
|
121
|
+
ale_homoscedastic_scale=float(ale_val),
|
|
122
|
+
)
|
|
123
|
+
for epi_val, ale_val in zip(epi_var_scale_values, ale_homoscedastic_values)
|
|
124
|
+
]
|
|
125
|
+
if params_warm_start is not None:
|
|
126
|
+
paramss.append(
|
|
127
|
+
ENNParams(
|
|
128
|
+
k=k,
|
|
129
|
+
epi_var_scale=params_warm_start.epi_var_scale,
|
|
130
|
+
ale_homoscedastic_scale=params_warm_start.ale_homoscedastic_scale,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
if len(paramss) == 0:
|
|
134
|
+
return ENNParams(k=k, epi_var_scale=1.0, ale_homoscedastic_scale=0.0)
|
|
135
|
+
import numpy as np
|
|
136
|
+
|
|
137
|
+
logliks = subsample_loglik(
|
|
138
|
+
model, train_x, y, paramss=paramss, P=num_fit_samples, rng=rng
|
|
139
|
+
)
|
|
140
|
+
if len(logliks) == 0:
|
|
141
|
+
return paramss[0]
|
|
142
|
+
best_idx = int(np.argmax(logliks))
|
|
143
|
+
return paramss[best_idx]
|
enn/enn/enn_normal.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ENNNormal:
|
|
12
|
+
mu: np.ndarray
|
|
13
|
+
se: np.ndarray
|
|
14
|
+
|
|
15
|
+
def sample(
|
|
16
|
+
self,
|
|
17
|
+
num_samples: int,
|
|
18
|
+
rng,
|
|
19
|
+
clip=None,
|
|
20
|
+
) -> np.ndarray:
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
size = (*self.se.shape, num_samples)
|
|
24
|
+
eps = rng.normal(size=size)
|
|
25
|
+
if clip is not None:
|
|
26
|
+
eps = np.clip(eps, a_min=-clip, a_max=clip)
|
|
27
|
+
return np.expand_dims(self.mu, -1) + np.expand_dims(self.se, -1) * eps
|
enn/enn/enn_params.py
ADDED
enn/enn/enn_util.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.random import Generator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def standardize_y(y: np.ndarray | list[float] | Any) -> tuple[float, float]:
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
y_array = np.asarray(y, dtype=float)
|
|
14
|
+
center = float(np.median(y_array))
|
|
15
|
+
scale = float(np.std(y_array))
|
|
16
|
+
if not np.isfinite(scale) or scale <= 0.0:
|
|
17
|
+
scale = 1.0
|
|
18
|
+
return center, scale
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def calculate_sobol_indices(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
if x.ndim != 2:
|
|
25
|
+
raise ValueError(f"x must be 2D, got shape {x.shape}")
|
|
26
|
+
n, d = x.shape
|
|
27
|
+
if d <= 0:
|
|
28
|
+
raise ValueError(f"x must have at least 1 dimension, got {d}")
|
|
29
|
+
if y.ndim == 2 and y.shape[1] == 1:
|
|
30
|
+
y = y.reshape(-1)
|
|
31
|
+
if y.ndim != 1:
|
|
32
|
+
raise ValueError(f"y must be 1D, got shape {y.shape}")
|
|
33
|
+
if y.shape[0] != n:
|
|
34
|
+
raise ValueError(f"y length {y.shape[0]} != x rows {n}")
|
|
35
|
+
if n < 9:
|
|
36
|
+
return np.ones(d, dtype=x.dtype)
|
|
37
|
+
mu = y.mean()
|
|
38
|
+
vy = y.var(ddof=0)
|
|
39
|
+
if not np.isfinite(vy) or vy <= 0:
|
|
40
|
+
return np.ones(d, dtype=x.dtype)
|
|
41
|
+
B = 10 if n >= 30 else 3
|
|
42
|
+
order = np.argsort(x, axis=0)
|
|
43
|
+
row_idx = np.arange(n).reshape(n, 1).repeat(d, axis=1)
|
|
44
|
+
ranks = np.empty_like(order)
|
|
45
|
+
ranks[order, np.arange(d)[None, :]] = row_idx
|
|
46
|
+
idx = (ranks * B) // n
|
|
47
|
+
oh = np.zeros((n, d, B), dtype=x.dtype)
|
|
48
|
+
oh[np.arange(n)[:, None], np.arange(d)[None, :], idx] = 1.0
|
|
49
|
+
counts = oh.sum(axis=0)
|
|
50
|
+
sums = (oh * y.reshape(n, 1, 1)).sum(axis=0)
|
|
51
|
+
mu_b = np.zeros_like(sums)
|
|
52
|
+
mask = counts > 0
|
|
53
|
+
mu_b[mask] = sums[mask] / counts[mask]
|
|
54
|
+
p_b = counts / float(n)
|
|
55
|
+
diff = mu_b - mu
|
|
56
|
+
S = (p_b * (diff * diff)).sum(axis=1) / vy
|
|
57
|
+
var_x = x.var(axis=0, ddof=0)
|
|
58
|
+
S = np.where(var_x <= 1e-12, np.zeros_like(S), S)
|
|
59
|
+
return S
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def arms_from_pareto_fronts(
|
|
63
|
+
x_cand: np.ndarray | Any,
|
|
64
|
+
mu: np.ndarray | Any,
|
|
65
|
+
se: np.ndarray | Any,
|
|
66
|
+
num_arms: int,
|
|
67
|
+
rng: Generator | Any,
|
|
68
|
+
) -> np.ndarray:
|
|
69
|
+
import numpy as np
|
|
70
|
+
from nds import ndomsort
|
|
71
|
+
|
|
72
|
+
if x_cand.ndim != 2:
|
|
73
|
+
raise ValueError(x_cand.shape)
|
|
74
|
+
if mu.shape != se.shape or mu.ndim != 1:
|
|
75
|
+
raise ValueError((mu.shape, se.shape))
|
|
76
|
+
if mu.size != x_cand.shape[0]:
|
|
77
|
+
raise ValueError((mu.size, x_cand.shape[0]))
|
|
78
|
+
|
|
79
|
+
combined = np.column_stack([mu, se])
|
|
80
|
+
idx_front = np.array(ndomsort.non_domin_sort(-combined, only_front_indices=True))
|
|
81
|
+
|
|
82
|
+
i_keep: list[int] = []
|
|
83
|
+
for n_front in range(1 + int(idx_front.max())):
|
|
84
|
+
front_indices = np.where(idx_front == n_front)[0]
|
|
85
|
+
front_indices = front_indices[np.argsort(-mu[front_indices])]
|
|
86
|
+
if len(i_keep) + len(front_indices) <= num_arms:
|
|
87
|
+
i_keep.extend(front_indices.tolist())
|
|
88
|
+
else:
|
|
89
|
+
remaining = num_arms - len(i_keep)
|
|
90
|
+
i_keep.extend(
|
|
91
|
+
rng.choice(front_indices, size=remaining, replace=False).tolist()
|
|
92
|
+
)
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
i_keep = np.array(i_keep)
|
|
96
|
+
return x_cand[i_keep[np.argsort(-mu[i_keep])]]
|
enn/turbo/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.random import Generator
|
|
8
|
+
|
|
9
|
+
from .turbo_config import TurboConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseTurboImpl:
|
|
13
|
+
def __init__(self, config: TurboConfig) -> None:
|
|
14
|
+
self._config = config
|
|
15
|
+
|
|
16
|
+
def get_x_center(
|
|
17
|
+
self,
|
|
18
|
+
x_obs_list: list,
|
|
19
|
+
y_obs_list: list,
|
|
20
|
+
rng: Generator,
|
|
21
|
+
) -> np.ndarray | None:
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from .turbo_utils import argmax_random_tie
|
|
25
|
+
|
|
26
|
+
y_array = np.asarray(y_obs_list, dtype=float)
|
|
27
|
+
if y_array.size == 0:
|
|
28
|
+
return None
|
|
29
|
+
idx = argmax_random_tie(y_array, rng=rng)
|
|
30
|
+
x_array = np.asarray(x_obs_list, dtype=float)
|
|
31
|
+
return x_array[idx]
|
|
32
|
+
|
|
33
|
+
def needs_tr_list(self) -> bool:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def create_trust_region(self, num_dim: int, num_arms: int) -> Any:
|
|
37
|
+
from .turbo_trust_region import TurboTrustRegion
|
|
38
|
+
|
|
39
|
+
return TurboTrustRegion(num_dim=num_dim, num_arms=num_arms)
|
|
40
|
+
|
|
41
|
+
def try_early_ask(
|
|
42
|
+
self,
|
|
43
|
+
num_arms: int,
|
|
44
|
+
x_obs_list: list,
|
|
45
|
+
draw_initial_fn: Callable[[int], np.ndarray],
|
|
46
|
+
get_init_lhd_points_fn: Callable[[int], np.ndarray | None],
|
|
47
|
+
) -> np.ndarray | None:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def handle_restart(
|
|
51
|
+
self,
|
|
52
|
+
x_obs_list: list,
|
|
53
|
+
y_obs_list: list,
|
|
54
|
+
yvar_obs_list: list,
|
|
55
|
+
init_idx: int,
|
|
56
|
+
num_init: int,
|
|
57
|
+
) -> tuple[bool, int]:
|
|
58
|
+
return False, init_idx
|
|
59
|
+
|
|
60
|
+
def prepare_ask(
|
|
61
|
+
self,
|
|
62
|
+
x_obs_list: list,
|
|
63
|
+
y_obs_list: list,
|
|
64
|
+
yvar_obs_list: list,
|
|
65
|
+
num_dim: int,
|
|
66
|
+
gp_num_steps: int,
|
|
67
|
+
rng: Any | None = None,
|
|
68
|
+
) -> tuple[Any, float | None, float | None, np.ndarray | None]:
|
|
69
|
+
return None, None, None, None
|
|
70
|
+
|
|
71
|
+
def select_candidates(
|
|
72
|
+
self,
|
|
73
|
+
x_cand: np.ndarray,
|
|
74
|
+
num_arms: int,
|
|
75
|
+
num_dim: int,
|
|
76
|
+
rng: Generator,
|
|
77
|
+
fallback_fn: Callable[[np.ndarray, int], np.ndarray],
|
|
78
|
+
from_unit_fn: Callable[[np.ndarray], np.ndarray],
|
|
79
|
+
) -> np.ndarray:
|
|
80
|
+
raise NotImplementedError("Subclasses must implement select_candidates")
|
|
81
|
+
|
|
82
|
+
def update_trust_region(
|
|
83
|
+
self,
|
|
84
|
+
tr_state: Any,
|
|
85
|
+
y_obs_list: list,
|
|
86
|
+
x_center: np.ndarray | None = None,
|
|
87
|
+
k: int | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
import numpy as np
|
|
90
|
+
|
|
91
|
+
y_obs_array = np.asarray(y_obs_list, dtype=float)
|
|
92
|
+
tr_state.update(y_obs_array)
|
|
93
|
+
|
|
94
|
+
def estimate_y(self, x_unit: np.ndarray, y_observed: np.ndarray) -> np.ndarray:
|
|
95
|
+
return y_observed
|
|
96
|
+
|
|
97
|
+
def get_mu_sigma(self, x_unit: np.ndarray) -> tuple[np.ndarray, np.ndarray] | None:
|
|
98
|
+
return None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.random import Generator
|
|
8
|
+
|
|
9
|
+
from .base_turbo_impl import BaseTurboImpl
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LHDOnlyImpl(BaseTurboImpl):
|
|
13
|
+
def get_x_center(
|
|
14
|
+
self,
|
|
15
|
+
x_obs_list: list,
|
|
16
|
+
y_obs_list: list,
|
|
17
|
+
rng: Generator,
|
|
18
|
+
) -> np.ndarray | None:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
def select_candidates(
|
|
22
|
+
self,
|
|
23
|
+
x_cand: np.ndarray,
|
|
24
|
+
num_arms: int,
|
|
25
|
+
num_dim: int,
|
|
26
|
+
rng: Generator,
|
|
27
|
+
fallback_fn: Callable[[np.ndarray, int], np.ndarray],
|
|
28
|
+
from_unit_fn: Callable[[np.ndarray], np.ndarray],
|
|
29
|
+
) -> np.ndarray:
|
|
30
|
+
from .turbo_utils import latin_hypercube
|
|
31
|
+
|
|
32
|
+
unit = latin_hypercube(num_arms, num_dim, rng=rng)
|
|
33
|
+
return from_unit_fn(unit)
|
|
34
|
+
|
|
35
|
+
def update_trust_region(
|
|
36
|
+
self,
|
|
37
|
+
tr_state: Any,
|
|
38
|
+
y_obs_list: list,
|
|
39
|
+
x_center: np.ndarray | None = None,
|
|
40
|
+
k: int | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
pass
|