Source code for saealib.surrogate.rbf
"""
RBF surrogate model module.
This module defines the Radial Basis Function (RBF) surrogate model.
RBFsurrogate supports multi-objective problems by maintaining one
independent RBF model per objective (ensemble approach).
"""
import logging
import numpy as np
import scipy.spatial
from saealib.surrogate.base import Surrogate
from saealib.surrogate.prediction import SurrogatePrediction
logger = logging.getLogger(__name__)
[docs]
def gaussian_kernel(x1: np.ndarray, x2: np.ndarray, sigma=2.0) -> np.ndarray:
"""
Gaussian radial basis function kernel.
Parameters
----------
x1 : np.ndarray
Input data 1.
x2 : np.ndarray
Input data 2.
sigma : float
Kernel width parameter.
Returns
-------
np.ndarray
Matrix of kernel evaluations between x1 and x2. shape: (len(x1), len(x2))
"""
sq_dist = scipy.spatial.distance.cdist(x1, x2, "sqeuclidean")
return np.exp(-sq_dist / (2 * (sigma**2)))
class _RBFModel:
"""
Single-objective RBF interpolation model (internal use only).
Holds all state for one objective's RBF fit. Used as a building
block by RBFsurrogate to support multi-objective problems.
"""
def __init__(self, kernel: callable, dim: int):
self.kernel = kernel
self.dim = dim
self.train_x: np.ndarray | None = None
self.train_y: np.ndarray | None = None
self.weights: np.ndarray | None = None
self.kernel_matrix: np.ndarray | None = None
self.sigma: float | None = None
def fit(self, train_x: np.ndarray, train_y_1d: np.ndarray) -> None:
"""
Fit the RBF model.
Parameters
----------
train_x : np.ndarray
Training input data. shape: (n_samples, n_features)
train_y_1d : np.ndarray
Training output data for one objective. shape: (n_samples,)
"""
self.train_x = np.asarray(train_x)
self.train_y = np.asarray(train_y_1d)
n_samples = len(train_x)
self.sigma = np.median(scipy.spatial.distance.pdist(self.train_x))
self.kernel_matrix = self.kernel(self.train_x, self.train_x, sigma=self.sigma)
rcond = 1 / np.linalg.cond(self.kernel_matrix)
if rcond < np.finfo(self.kernel_matrix.dtype).eps:
logger.warning(f"Kernel matrix is ill-conditioned. RCOND: {rcond}")
try:
self.weights = np.linalg.solve(
self.kernel_matrix, (self.train_y - np.mean(self.train_y))
)
except np.linalg.LinAlgError:
logger.error(
"Failed to solve linear system (Kernel matrix might be singular)."
)
self.weights = np.nan * np.ones(n_samples)
def predict(self, test_x: np.ndarray) -> np.ndarray:
"""
Predict for one objective.
Parameters
----------
test_x : np.ndarray
Input data. shape: (n_samples, n_features)
Returns
-------
np.ndarray
Predicted values. shape: (n_samples,)
"""
test = np.asarray(test_x)
if test.ndim == 1:
test = test.reshape(1, -1)
k = self.kernel(self.train_x, test, sigma=self.sigma)
preds = k.T.dot(self.weights) + np.mean(self.train_y)
return np.asarray(preds).flatten()
[docs]
class RBFsurrogate(Surrogate):
"""
Radial Basis Function (RBF) Interpolation surrogate model.
Supports multi-objective problems by maintaining one independent
_RBFModel per objective. The number of objectives is inferred from
``train_y`` on the first call to ``fit`` (lazy initialization).
Attributes
----------
kernel : callable
Kernel function (e.g. gaussian_kernel).
dim : int
Dimensionality of the input data.
n_obj : int or None
Number of objectives. Set on first fit call.
"""
[docs]
def __init__(self, kernel: callable, dim: int):
self.kernel = kernel
self.dim = dim
self.n_obj: int | None = None
self._models: list[_RBFModel] | None = None
[docs]
def fit(self, train_x: np.ndarray, train_y: np.ndarray) -> None:
"""
Fit the surrogate model.
Parameters
----------
train_x : np.ndarray
Training input data. shape: (n_samples, n_features)
train_y : np.ndarray
Training output data. shape: (n_samples, n_obj) or (n_samples,).
1-D input is treated as single-objective: shape (n_samples, 1).
"""
arr = np.asarray(train_y, dtype=float)
if arr.ndim == 1:
arr = arr.reshape(-1, 1) # (n_samples,) -> (n_samples, 1)
n_obj = arr.shape[1]
# (Re-)initialize models when n_obj changes or on first fit
if self._models is None or n_obj != self.n_obj:
self.n_obj = n_obj
self._models = [_RBFModel(self.kernel, self.dim) for _ in range(n_obj)]
for i, model in enumerate(self._models):
model.fit(train_x, arr[:, i])
[docs]
def predict(self, test_x: np.ndarray) -> SurrogatePrediction:
"""
Predict using the surrogate model.
Parameters
----------
test_x : np.ndarray
Input data. shape: (n_samples, n_features) or (n_features,)
Returns
-------
SurrogatePrediction
prediction.value shape: (n_samples, n_obj)
prediction.std is None (RBF interpolation provides no uncertainty)
"""
preds = [m.predict(test_x) for m in self._models]
value = np.column_stack(preds) # (n_samples, n_obj)
return SurrogatePrediction(value=value)