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 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
@@ -0,0 +1,4 @@
1
+ from .enn import EpistemicNearestNeighbors
2
+ from .enn_fit import enn_fit
3
+
4
+ __all__: list[str] = ["EpistemicNearestNeighbors", "enn_fit"]
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
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class ENNParams:
8
+ k: int
9
+ epi_var_scale: float
10
+ ale_homoscedastic_scale: float
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,11 @@
1
+ from .turbo_mode import TurboMode
2
+ from .turbo_optimizer import Telemetry, TurboOptimizer
3
+
4
+ Turbo = TurboOptimizer
5
+
6
+ __all__ = [
7
+ "TurboMode",
8
+ "TurboOptimizer",
9
+ "Turbo",
10
+ "Telemetry",
11
+ ]
@@ -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