Source code for pytwinnet.optimization.bayesopt

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable, List, Tuple, Any
import numpy as np
import math
from typing import Dict

[docs] def rbf_kernel(X1: np.ndarray, X2: np.ndarray, lengthscale: float, variance: float) -> np.ndarray: D = ((X1[:, None, :] - X2[None, :, :]) ** 2).sum(axis=2) return variance * np.exp(-0.5 * D / (lengthscale ** 2 + 1e-12))
[docs] @dataclass class SimpleBayesOpt: """Tiny Bayesian Optimization (RBF GP + Expected Improvement).""" bounds: List[Tuple[float, float]] init_points: int = 8 iters: int = 32 lengthscale: float = 0.5 variance: float = 1.0 noise: float = 1e-6 seed: int = 0 X: List[List[float]] = field(default_factory=list) y: List[float] = field(default_factory=list)
[docs] def ask(self, n: int = 1) -> List[List[float]]: rng = np.random.default_rng(self.seed + len(self.X)) pts = [] for _ in range(n): x = [rng.uniform(a, b) for (a, b) in self.bounds] pts.append(x) return pts
[docs] def tell(self, X: List[List[float]], y: List[float]) -> None: self.X.extend(X); self.y.extend(y)
def _posterior(self, Xs: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: if not self.X: mu = np.zeros((Xs.shape[0],)); s2 = np.ones_like(mu) return mu, s2 X = np.array(self.X, float); y = np.array(self.y, float) K = rbf_kernel(X, X, self.lengthscale, self.variance) + self.noise * np.eye(X.shape[0]) Ks = rbf_kernel(X, Xs, self.lengthscale, self.variance) Kss = rbf_kernel(Xs, Xs, self.lengthscale, self.variance) + self.noise * np.eye(Xs.shape[0]) L = np.linalg.cholesky(K) alpha = np.linalg.solve(L.T, np.linalg.solve(L, y)) mu = Ks.T @ alpha v = np.linalg.solve(L, Ks) s2 = np.maximum(np.diag(Kss) - (v * v).sum(axis=0), 1e-12) return mu, s2 def _ei(self, mu: np.ndarray, s2: np.ndarray, y_best: float) -> np.ndarray: # Vectorized Expected Improvement s = np.sqrt(s2) z = (mu - y_best) / (s + 1e-12) erf = np.vectorize(math.erf) Phi = 0.5 * (1.0 + erf(z / np.sqrt(2.0))) # CDF of N(0,1) phi = (1.0 / np.sqrt(2.0 * np.pi)) * np.exp(-0.5 * z * z) # PDF of N(0,1) return (mu - y_best) * Phi + s * phi
[docs] def suggest(self, n: int = 1) -> List[List[float]]: rng = np.random.default_rng(self.seed + 1234 + len(self.X)) cand = rng.uniform( low=np.array([a for a, _ in self.bounds]), high=np.array([b for _, b in self.bounds]), size=(1024, len(self.bounds)), ) mu, s2 = self._posterior(cand) y_best = max(self.y) if self.y else 0.0 ei = self._ei(mu, s2, y_best) idx = np.argsort(-ei)[:n] return cand[idx].tolist()
[docs] def run(self, evaluate: Callable[[List[float]], float]) -> Dict[str, Any]: X0 = self.ask(self.init_points) y0 = [evaluate(x) for x in X0] self.tell(X0, y0) for _ in range(self.iters): Xn = self.suggest(1) yn = [evaluate(x) for x in Xn] self.tell(Xn, yn) best_i = int(np.argmax(self.y)) return {"best_x": self.X[best_i], "best_y": float(self.y[best_i]), "n_evals": len(self.y)}