Skip to content

Commit 6a80322

Browse files
0.35.0
Kriging Kernel
1 parent 4e88bd8 commit 6a80322

3 files changed

Lines changed: 146 additions & 43 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
77

88
[project]
99
name = "spotpython"
10-
version = "0.34.11"
10+
version = "0.35.0"
1111
authors = [
1212
{ name="T. Bartz-Beielstein", email="tbb@bartzundbartz.de" }
1313
]

src/spotpython/spot/spot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2325,7 +2325,7 @@ def infill(self, x) -> float:
23252325
(numpy.ndarray): value based on infill criterion, e.g., `"ei"`. Shape `(1,)`.
23262326
The objective function value `y` that is used as a base value for the
23272327
infill criterion is calculated in natural units.
2328-
2328+
23292329
Examples:
23302330
>>> import numpy as np
23312331
from spotpython.spot import Spot

src/spotpython/surrogate/kriging.py

Lines changed: 144 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import numpy as np
22
from numpy.linalg import LinAlgError, cond
3-
from typing import Dict, Tuple, List, Optional
3+
from typing import Dict, Tuple, List, Optional, Callable, Union
44
from scipy.optimize import differential_evolution
55
from sklearn.base import BaseEstimator, RegressorMixin
66
from scipy.special import erf
@@ -9,18 +9,68 @@
99
from scipy.spatial.distance import cdist, pdist, squareform
1010
from spotpython.surrogate.plot import plotkd
1111

12+
# --- Kernel functions ---
13+
14+
15+
def gauss_kernel(D):
16+
"""Gaussian (RBF) kernel: exp(-D)"""
17+
return np.exp(-D)
18+
19+
20+
def matern_kernel(D, nu=2.5):
21+
"""Matern kernel (default nu=2.5, smooth)."""
22+
if nu == 0.5:
23+
return np.exp(-np.sqrt(D))
24+
elif nu == 1.5:
25+
sqrt3D = np.sqrt(3.0 * D)
26+
return (1.0 + sqrt3D) * np.exp(-sqrt3D)
27+
elif nu == 2.5:
28+
sqrt5D = np.sqrt(5.0 * D)
29+
return (1.0 + sqrt5D + (5.0 / 3.0) * D) * np.exp(-sqrt5D)
30+
else:
31+
# Fallback to Gaussian for unsupported nu
32+
return np.exp(-D)
33+
34+
35+
def exponential_kernel(D):
36+
"""Exponential kernel: exp(-sqrt(D))"""
37+
return np.exp(-np.sqrt(D))
38+
39+
40+
def cubic_kernel(D):
41+
"""Cubic kernel: 1 - D^3"""
42+
return 1.0 - D**3
43+
44+
45+
def linear_kernel(D):
46+
"""Linear kernel: 1 - D"""
47+
return 1.0 - D
48+
49+
50+
def rational_quadratic_kernel(D, alpha=1.0):
51+
"""Rational Quadratic kernel: (1 + D/(2*alpha))^(-alpha)"""
52+
return (1.0 + D / (2.0 * alpha)) ** (-alpha)
53+
54+
55+
def poly_kernel(D, degree=2):
56+
"""Polynomial kernel: (1 + D)^degree"""
57+
return (1.0 + D) ** degree
58+
59+
1260
# --- The New Kriging Class with Nyström Approximation as introduced in v0.34.0 ---
1361

1462

1563
class Kriging(BaseEstimator, RegressorMixin):
1664
"""
1765
A scikit-learn compatible Kriging model class for regression tasks,
18-
extended with an optional Nyström approximation for scaling.
66+
extended with an optional Nyström approximation for scaling and explicit kernel selection.
1967
2068
Public API and core behavior match src/spotpython/surrogate/kriging.py.
2169
The same basis function/correlation definitions are used. When use_nystrom=True,
2270
training and prediction use low-rank Woodbury solves based on m inducing points.
2371
72+
Kernel selection is explicit via the `kernel` and `kernel_params` arguments.
73+
2474
Attributes:
2575
eps (float): A small regularization term to reduce ill-conditioning.
2676
penalty (float): The penalty value used if the correlation matrix is ill-conditioned.
@@ -35,11 +85,28 @@ class Kriging(BaseEstimator, RegressorMixin):
3585
use_nystrom (bool): If True, use Nyström low-rank solves.
3686
nystrom_m (int): Number of inducing points (landmarks).
3787
nystrom_seed (int): RNG seed for landmark selection.
88+
kernel (str or callable): Kernel type ("gauss", "matern", "exp", "cubic", "linear", "rq", "poly") or a custom callable.
89+
kernel_params (dict): Parameters for the kernel (e.g., nu for Matern, alpha for rational quadratic, degree for poly).
3890
3991
Notes (Forrester et al., Engineering Design via Surrogate Modelling, Ch. 2/3/6):
40-
- Correlation: R = exp(-D), with D weighted distances (Ch. 2).
92+
- Correlation: R = kernel(D), with D weighted distances (Ch. 2).
4193
- Ordinary Kriging μ, σ^2, concentrated likelihood (Ch. 3).
4294
- Prediction f̂, variance s^2, and Expected Improvement (Ch. 3 & 6).
95+
96+
Examples:
97+
>>> import numpy as np
98+
>>> from spotpython.surrogate.kriging import Kriging
99+
>>> X_train = np.array([[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]])
100+
>>> y_train = np.array([0.1, 0.2, 0.3])
101+
>>> # Gaussian kernel (default)
102+
>>> model = Kriging(kernel="gauss").fit(X_train, y_train)
103+
>>> # Matern kernel (nu=1.5)
104+
>>> model_matern = Kriging(kernel="matern", kernel_params={"nu": 1.5}).fit(X_train, y_train)
105+
>>> # Rational Quadratic kernel
106+
>>> model_rq = Kriging(kernel="rq", kernel_params={"alpha": 2.0}).fit(X_train, y_train)
107+
>>> # Custom kernel
108+
>>> def custom_kernel(D): return np.exp(-D**2)
109+
>>> model_custom = Kriging(kernel=custom_kernel).fit(X_train, y_train)
43110
"""
44111

45112
def __init__(
@@ -74,9 +141,13 @@ def __init__(
74141
use_nystrom: bool = False,
75142
nystrom_m: Optional[int] = None,
76143
nystrom_seed: int = 1234,
144+
# Kernel options
145+
kernel: Union[str, Callable] = "gauss",
146+
kernel_params: Optional[dict] = None,
77147
**kwargs,
78148
):
79-
"""Initialize the Kriging model.
149+
"""
150+
Initialize the Kriging model.
80151
81152
Args:
82153
eps (float): Small regularization term for numerical stability.
@@ -108,6 +179,8 @@ def __init__(
108179
use_nystrom (bool): If True, use Nyström approximation.
109180
nystrom_m (Optional[int]): Number of landmarks for Nyström.
110181
nystrom_seed (int): Seed for landmark selection in Nyström.
182+
kernel (str or callable): Kernel type ("gauss", "matern", "exp", "cubic", "linear", "rq", "poly") or a custom callable.
183+
kernel_params (dict): Parameters for the kernel (e.g., nu for Matern, alpha for rational quadratic, degree for poly).
111184
"""
112185
if eps is None:
113186
self.eps = self._get_eps()
@@ -140,6 +213,10 @@ def __init__(
140213
self.model_optimizer = model_optimizer or differential_evolution
141214
self.model_fun_evals = model_fun_evals or 100
142215

216+
# Kernel selection
217+
self.kernel = kernel
218+
self.kernel_params = kernel_params or {}
219+
143220
# Logging information
144221
self.log = {}
145222
self.log["negLnLike"] = []
@@ -180,6 +257,42 @@ def __init__(
180257
self.Rinv_one_ = None # R^{-1} 1 (n,)
181258
self.Rinv_r_ = None # R^{-1} r (n,)
182259

260+
# --- Kernel dispatch ---
261+
262+
def _correlation(self, D):
263+
"""
264+
Dispatches to the selected kernel function.
265+
266+
Args:
267+
D (np.ndarray): Distance matrix.
268+
269+
Returns:
270+
np.ndarray: Correlation matrix.
271+
"""
272+
if callable(self.kernel):
273+
return self.kernel(D, **self.kernel_params)
274+
elif self.kernel == "gauss":
275+
return gauss_kernel(D)
276+
elif self.kernel == "matern":
277+
nu = self.kernel_params.get("nu", 2.5)
278+
return matern_kernel(D, nu=nu)
279+
elif self.kernel == "exp":
280+
return exponential_kernel(D)
281+
elif self.kernel == "cubic":
282+
return cubic_kernel(D)
283+
elif self.kernel == "linear":
284+
return linear_kernel(D)
285+
elif self.kernel == "rq":
286+
alpha = self.kernel_params.get("alpha", 1.0)
287+
return rational_quadratic_kernel(D, alpha=alpha)
288+
elif self.kernel == "poly":
289+
degree = self.kernel_params.get("degree", 2)
290+
return poly_kernel(D, degree=degree)
291+
else:
292+
raise ValueError(f"Unknown kernel: {self.kernel}")
293+
294+
# -------- Basis correlation construction (identical to kriging.py) --------
295+
183296
def _get_eps(self) -> float:
184297
"""
185298
Computes a small epsilon value for numerical stability.
@@ -325,14 +438,13 @@ def build_Psi(self) -> np.ndarray:
325438
Constructs a new (n x n) correlation matrix Psi to reflect new data
326439
or a change in hyperparameters.
327440
This method uses `theta`, `p`, and coded `X` values to construct the
328-
correlation matrix as described in [Forr08a, p.57].
441+
correlation matrix as described in [Forrester et al., p.57].
329442
330443
Notes:
331-
- Correlation follows the stationary Gaussian kernel used in Kriging:
332-
R = exp(-D), with D a weighted distance. See Forrester et al. (2008),
333-
Ch. 2, correlation modelling.
444+
- Correlation follows the selected kernel:
445+
R = kernel(D), with D a weighted distance.
334446
- The code builds D as a sum of per-dimension distance contributions
335-
scaled by 10**theta (theta is stored in log10), then applies exp(-D).
447+
scaled by 10**theta (theta is stored in log10), then applies the kernel.
336448
- Returns only the upper triangle; the symmetric and diagonal parts
337449
are handled by the caller.
338450
@@ -350,9 +462,8 @@ def build_Psi(self) -> np.ndarray:
350462
>>> # Training data
351463
>>> X_train = np.array([[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]])
352464
>>> y_train = np.array([0.1, 0.2, 0.3])
353-
>>> # Fit the Kriging model
354-
>>> model = Kriging().fit(X_train, y_train)
355-
>>> # Build the correlation matrix Psi
465+
>>> # Fit the Kriging model with a Matern kernel
466+
>>> model = Kriging(kernel="matern", kernel_params={"nu": 1.5}).fit(X_train, y_train)
356467
>>> Psi = model.build_Psi()
357468
>>> print("Correlation matrix Psi:\n", Psi)
358469
"""
@@ -372,7 +483,7 @@ def build_Psi(self) -> np.ndarray:
372483
D_factor = squareform(pdist(X_factor, metric=self.metric_factorial, w=theta10[self.factor_mask]))
373484
Psi += D_factor
374485

375-
Psi = np.exp(-Psi)
486+
Psi = self._correlation(Psi)
376487

377488
self.inf_Psi = np.isinf(Psi).any()
378489
self.cnd_Psi = cond(Psi)
@@ -385,7 +496,7 @@ def build_Psi(self) -> np.ndarray:
385496
def _kernel_cross(self, A: np.ndarray, B: np.ndarray) -> np.ndarray:
386497
"""
387498
Cross-correlation matrix K(A,B) using the same distance definition as build_Psi/build_psi_vec.
388-
Returns exp(-D) with D composed from ordered and factor contributions.
499+
Returns kernel(D) with D composed from ordered and factor contributions.
389500
390501
Args:
391502
A (np.ndarray): First set of points (m x k).
@@ -396,17 +507,14 @@ def _kernel_cross(self, A: np.ndarray, B: np.ndarray) -> np.ndarray:
396507
397508
Examples:
398509
>>> import numpy as np
399-
from spotpython.surrogate.kriging import Kriging
400-
X_train = np.array([[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]])
401-
y_train = np.array([0.1, 0.2, 0.3])
402-
# Fit the Kriging model
403-
model = Kriging().fit(X_train, y_train)
404-
# Create two sets of points A and B
405-
A = np.array([[0.0, 0.0], [1.0, 1.0]])
406-
B = np.array([[0.5, 0.5], [1.5, 1.5]])
407-
# Compute the cross-correlation matrix K(A, B)
408-
K_AB = model._kernel_cross(A, B)
409-
print("Cross-correlation matrix K(A, B):\n", K_AB)
510+
>>> from spotpython.surrogate.kriging import Kriging
511+
>>> X_train = np.array([[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]])
512+
>>> y_train = np.array([0.1, 0.2, 0.3])
513+
>>> model = Kriging(kernel="poly", kernel_params={"degree": 3}).fit(X_train, y_train)
514+
>>> A = np.array([[0.0, 0.0], [1.0, 1.0]])
515+
>>> B = np.array([[0.5, 0.5], [1.5, 1.5]])
516+
>>> K_AB = model._kernel_cross(A, B)
517+
>>> print("Cross-correlation matrix K(A, B):\n", K_AB)
410518
"""
411519
A = np.asarray(A)
412520
B = np.asarray(B)
@@ -426,34 +534,29 @@ def _kernel_cross(self, A: np.ndarray, B: np.ndarray) -> np.ndarray:
426534
Bf = B[:, self.factor_mask]
427535
D += cdist(Af, Bf, metric=self.metric_factorial, w=theta10[self.factor_mask])
428536

429-
return np.exp(-D)
537+
return self._correlation(D)
430538

431539
def build_psi_vec(self, x: np.ndarray) -> np.ndarray:
432540
"""
433541
Build the psi vector required for predictive methods.
434-
ψ(x) := [exp(-D(x, x_i))]_{i=1..n}, i.e., correlation between a new x and the training sites
435-
using the same D as for R (Forrester, Ch. 2).
542+
ψ(x) := [kernel(D(x, x_i))]_{i=1..n}, i.e., correlation between a new x and the training sites
543+
using the same D as for R.
436544
437545
Args:
438546
x (ndarray): Point to calculate the psi vector for.
439547
440548
Returns:
441-
None
442-
443-
Modifies:
444-
self.psi (np.ndarray): Updates the psi vector.
549+
np.ndarray: The psi vector.
445550
446551
Examples:
447552
>>> import numpy as np
448-
from spotpython.surrogate.kriging import Kriging
449-
# Training data
450-
X_train = np.array([[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]])
451-
y_train = np.array([0.1, 0.2, 0.3])
452-
# Fit the Kriging model
453-
model = Kriging().fit(X_train, y_train)
454-
x_new = np.array([0.25, 0.25])
455-
psi_vector = model.build_psi_vec(x_new)
456-
print("Psi vector for new point:\n", psi_vector)
553+
>>> from spotpython.surrogate.kriging import Kriging
554+
>>> X_train = np.array([[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]])
555+
>>> y_train = np.array([0.1, 0.2, 0.3])
556+
>>> model = Kriging(kernel="rq", kernel_params={"alpha": 2.0}).fit(X_train, y_train)
557+
>>> x_new = np.array([0.25, 0.25])
558+
>>> psi_vector = model.build_psi_vec(x_new)
559+
>>> print("Psi vector for new point:\n", psi_vector)
457560
"""
458561
try:
459562
psi = self._kernel_cross(np.asarray(x), self.X_).ravel()

0 commit comments

Comments
 (0)