Skip to content

Commit 3d76463

Browse files
0.33.13
1 parent bc25bd1 commit 3d76463

5 files changed

Lines changed: 476 additions & 45 deletions

File tree

notebooks/00_spotPython_tests.ipynb

Lines changed: 122 additions & 31 deletions
Large diffs are not rendered by default.

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.33.12"
10+
version = "0.33.13"
1111
authors = [
1212
{ name="T. Bartz-Beielstein", email="tbb@bartzundbartz.de" }
1313
]

src/spotpython/surrogate/kriging.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,20 @@ def predict(self, X: np.ndarray, return_std=False, return_val: str = "y") -> np.
451451
>>> print("Predictions:", y_pred)
452452
"""
453453
self.return_std = return_std
454-
X = np.atleast_2d(X)
454+
X = np.asarray(X)
455+
456+
# Ensure X has shape (n_samples, n_features=self.k)
457+
if X.ndim == 1:
458+
X = X.reshape(-1, self.k)
459+
else:
460+
if X.shape[1] != self.k:
461+
if X.shape[0] == self.k: # common case: row/col swap for 1D
462+
X = X.T
463+
elif self.k == 1:
464+
X = X.reshape(-1, 1)
465+
else:
466+
raise ValueError(f"X has shape {X.shape}, expected (*, {self.k}).")
467+
455468
if return_std:
456469
# Return predictions and standard deviations
457470
# Compatibility with scikit-learn
@@ -549,8 +562,7 @@ def likelihood(self, x: np.ndarray) -> Tuple[float, np.ndarray, np.ndarray]:
549562
550563
Args:
551564
x (np.ndarray):
552-
1D array of log(theta) parameters of length k. If self.method is "regression" or
553-
"reinterpolation", length is k+1 and the last element of x is the log(noise) parameter.
565+
1D array of log(theta), log(Lambda) (if method is "regression" or "reinterpolation"), and p values (if optim_p is True).
554566
555567
Returns:
556568
(float, np.ndarray, np.ndarray):
@@ -762,11 +774,24 @@ def max_likelihood(self, bounds: List[Tuple[float, float]]) -> Tuple[np.ndarray,
762774
763775
Returns:
764776
(np.ndarray, float): (best_x, best_fun) where best_x is the
765-
optimal log(theta) array and best_fun is the minimized negative log-likelihood.
777+
optimal [log(theta) log(lambda) p] array and best_fun is the minimized negative log-likelihood.
778+
779+
Examples:
780+
>>> import numpy as np
781+
from spotpython.surrogate.kriging import Kriging
782+
# Training data
783+
X_train = np.array([[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]])
784+
y_train = np.array([0.1, 0.2, 0.3])
785+
# Fit the Kriging model
786+
model = Kriging().fit(X_train, y_train)
787+
bounds = [(-3.0, 2.0), (-3.0, 2.0), (-9.0, 2.0)] # Example bounds for log(theta) and log(lambda)
788+
best_x, best_fun = model.max_likelihood(bounds)
789+
print("Optimal parameters (log(theta),log(theta), log(lambda)):", best_x)
790+
print("Minimized negative log-likelihood:", best_fun)
766791
"""
767792

768-
def objective(logtheta_lambda):
769-
neg_ln_like, _, _ = self.likelihood(logtheta_lambda)
793+
def objective(logtheta_loglambda_p: np.ndarray) -> float:
794+
neg_ln_like, _, _ = self.likelihood(logtheta_loglambda_p)
770795
return neg_ln_like
771796

772797
result = differential_evolution(objective, bounds)
@@ -793,17 +818,17 @@ def plot(self, i: int = 0, j: int = 1, show: Optional[bool] = True, add_points:
793818
Returns:
794819
None
795820
796-
Note:
797-
* This method provides only a basic plot. For more advanced plots,
798-
use the `plot_contour()` method of the `Spot` class.
821+
Notes:
822+
* This method is a wrapper around the `plotkd` function for 2D plots.
823+
* For 1D plots, it generates a line plot of the surrogate model.
799824
800825
Examples:
801826
>>> import numpy as np
802827
from spotpython.fun.objectivefunctions import Analytical
803828
from spotpython.spot import spot
804829
from spotpython.utils.init import fun_control_init, design_control_init
805830
# 1-dimensional example
806-
fun = analytical().fun_sphere
831+
fun = Analytical().fun_sphere
807832
fun_control=fun_control_init(lower = np.array([-1]),
808833
upper = np.array([1]),
809834
noise=False)
@@ -816,7 +841,7 @@ def plot(self, i: int = 0, j: int = 1, show: Optional[bool] = True, add_points:
816841
S.fit_surrogate()
817842
S.surrogate.plot()
818843
# 2-dimensional example
819-
fun = analytical().fun_sphere
844+
fun = Analytical().fun_sphere
820845
fun_control=fun_control_init(lower = np.array([-1, -1]),
821846
upper = np.array([1, 1]),
822847
noise=False)
@@ -831,10 +856,15 @@ def plot(self, i: int = 0, j: int = 1, show: Optional[bool] = True, add_points:
831856
"""
832857
if self.k == 1:
833858
n_grid = 100
834-
x = linspace(self.min_X[0], self.max_X[0], num=n_grid)
859+
x = linspace(self.min_X[0], self.max_X[0], num=n_grid).reshape(-1, 1)
835860
y = self.predict(x)
836861
plt.figure()
837-
plt.plot(x, y, "k")
862+
plt.plot(x.ravel(), y, "k")
863+
# add the original points
864+
if add_points:
865+
plt.plot(self.X_[:, 0], self.y_, "ro")
866+
# add grid
867+
plt.grid()
838868
if show:
839869
plt.show()
840870
else:

test/test_fit_kriging.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import numpy as np
2+
import pytest
3+
4+
from spotpython.surrogate.kriging import Kriging
5+
6+
7+
def _make_xy(k=2):
8+
X = np.array([[0.0] * k, [0.5] * k, [1.0] * k], dtype=float)
9+
y = np.array([0.1, 0.2, 0.3], dtype=float)
10+
return X, y
11+
12+
13+
def test_fit_regression_default_bounds_and_state_updated(monkeypatch):
14+
X, y = _make_xy(k=2)
15+
model = Kriging(method="regression")
16+
17+
seen = {"bounds": None, "x_likelihood": None}
18+
19+
def fake_max_likelihood(bounds):
20+
seen["bounds"] = bounds
21+
# k=2 thetas + 1 lambda
22+
return np.array([0.1, -0.2, -6.0], dtype=float), -999.0
23+
24+
def fake_likelihood(x):
25+
seen["x_likelihood"] = x
26+
n = X.shape[0]
27+
return 3.14, np.eye(n), np.eye(n)
28+
29+
monkeypatch.setattr(model, "max_likelihood", fake_max_likelihood, raising=True)
30+
monkeypatch.setattr(model, "likelihood", fake_likelihood, raising=True)
31+
32+
ret = model.fit(X, y)
33+
34+
assert ret is model
35+
36+
expected_bounds = [(model.min_theta, model.max_theta)] * X.shape[1] + [
37+
(model.min_Lambda, model.max_Lambda)
38+
]
39+
assert seen["bounds"] == expected_bounds
40+
41+
assert np.allclose(model.theta, np.array([0.1, -0.2]))
42+
assert np.allclose(model.Lambda, np.array([-6.0]))
43+
assert np.allclose(model.logtheta_lambda_, np.array([0.1, -0.2, -6.0]))
44+
assert model.negLnLike == pytest.approx(3.14)
45+
assert np.allclose(model.Psi_, np.eye(X.shape[0]))
46+
assert np.allclose(model.U_, np.eye(X.shape[0]))
47+
assert np.allclose(seen["x_likelihood"], np.array([0.1, -0.2, -6.0]))
48+
49+
50+
def test_fit_interpolation_bounds_no_lambda_and_lambda_none(monkeypatch):
51+
X, y = _make_xy(k=2)
52+
model = Kriging(method="interpolation")
53+
54+
seen = {"bounds": None, "x_likelihood": None}
55+
56+
def fake_max_likelihood(bounds):
57+
seen["bounds"] = bounds
58+
# Only k=2 thetas
59+
return np.array([0.25, -0.75], dtype=float), -1.0
60+
61+
def fake_likelihood(x):
62+
seen["x_likelihood"] = x
63+
n = X.shape[0]
64+
return 1.23, np.eye(n), np.eye(n)
65+
66+
monkeypatch.setattr(model, "max_likelihood", fake_max_likelihood, raising=True)
67+
monkeypatch.setattr(model, "likelihood", fake_likelihood, raising=True)
68+
69+
model.fit(X, y)
70+
71+
expected_bounds = [(model.min_theta, model.max_theta)] * X.shape[1]
72+
assert seen["bounds"] == expected_bounds
73+
74+
assert np.allclose(model.theta, np.array([0.25, -0.75]))
75+
assert model.Lambda is None
76+
assert np.allclose(model.logtheta_lambda_, np.array([0.25, -0.75]))
77+
assert model.negLnLike == pytest.approx(1.23)
78+
assert np.allclose(model.Psi_, np.eye(X.shape[0]))
79+
assert np.allclose(model.U_, np.eye(X.shape[0]))
80+
assert np.allclose(seen["x_likelihood"], np.array([0.25, -0.75]))
81+
82+
83+
def test_fit_reinterpolation_bounds_and_state_updated(monkeypatch):
84+
X, y = _make_xy(k=3)
85+
model = Kriging(method="reinterpolation")
86+
87+
seen = {"bounds": None}
88+
89+
def fake_max_likelihood(bounds):
90+
seen["bounds"] = bounds
91+
# k=3 thetas + 1 lambda
92+
return np.array([0.0, 0.1, 0.2, -3.0], dtype=float), -5.0
93+
94+
def fake_likelihood(x):
95+
n = X.shape[0]
96+
return 0.77, np.eye(n), np.eye(n)
97+
98+
monkeypatch.setattr(model, "max_likelihood", fake_max_likelihood, raising=True)
99+
monkeypatch.setattr(model, "likelihood", fake_likelihood, raising=True)
100+
101+
model.fit(X, y)
102+
103+
expected_bounds = [(model.min_theta, model.max_theta)] * X.shape[1] + [
104+
(model.min_Lambda, model.max_Lambda)
105+
]
106+
assert seen["bounds"] == expected_bounds
107+
108+
assert np.allclose(model.theta, np.array([0.0, 0.1, 0.2]))
109+
assert np.allclose(model.Lambda, np.array([-3.0]))
110+
assert np.allclose(model.logtheta_lambda_, np.array([0.0, 0.1, 0.2, -3.0]))
111+
assert model.negLnLike == pytest.approx(0.77)
112+
assert np.allclose(model.Psi_, np.eye(X.shape[0]))
113+
assert np.allclose(model.U_, np.eye(X.shape[0]))
114+
115+
116+
def test_fit_isotropic_uses_k_theta_bounds_but_sets_n_theta_1(monkeypatch):
117+
X, y = _make_xy(k=3)
118+
model = Kriging(method="regression", isotropic=True)
119+
120+
seen = {"bounds": None}
121+
122+
def fake_max_likelihood(bounds):
123+
seen["bounds"] = bounds
124+
# despite isotropic, code builds k thetas + 1 lambda bounds
125+
return np.array([0.5, 0.4, 0.3, -4.0], dtype=float), -2.0
126+
127+
def fake_likelihood(x):
128+
n = X.shape[0]
129+
return 2.22, np.eye(n), np.eye(n)
130+
131+
monkeypatch.setattr(model, "max_likelihood", fake_max_likelihood, raising=True)
132+
monkeypatch.setattr(model, "likelihood", fake_likelihood, raising=True)
133+
134+
model.fit(X, y)
135+
136+
expected_bounds = [(model.min_theta, model.max_theta)] * X.shape[1] + [
137+
(model.min_Lambda, model.max_Lambda)
138+
]
139+
assert seen["bounds"] == expected_bounds
140+
assert model.n_theta == 1
141+
# Only first theta retained in model.theta for isotropic
142+
assert np.allclose(model.theta, np.array([0.5]))
143+
# Lambda is taken from the first param after n_theta
144+
assert np.allclose(model.Lambda, np.array([0.4]))
145+
# logtheta_lambda_ still holds all optimized parameters
146+
assert np.allclose(model.logtheta_lambda_, np.array([0.5, 0.4, 0.3, -4.0]))
147+
148+
149+
def test_fit_respects_explicit_bounds_override(monkeypatch):
150+
X, y = _make_xy(k=2)
151+
model = Kriging(method="regression")
152+
153+
seen = {"bounds": None, "x_likelihood": None}
154+
155+
def fake_max_likelihood(bounds):
156+
seen["bounds"] = bounds
157+
# Must match provided bounds length (2 theta + 1 lambda)
158+
return np.array([1.0, -1.0, -2.0], dtype=float), -7.0
159+
160+
def fake_likelihood(x):
161+
seen["x_likelihood"] = x
162+
n = X.shape[0]
163+
return 4.56, np.eye(n), np.eye(n)
164+
165+
monkeypatch.setattr(model, "max_likelihood", fake_max_likelihood, raising=True)
166+
monkeypatch.setattr(model, "likelihood", fake_likelihood, raising=True)
167+
168+
custom_bounds = [(-1.0, 1.0), (-2.0, 2.0), (-8.0, -1.0)]
169+
model.fit(X, y, bounds=custom_bounds)
170+
171+
assert seen["bounds"] == custom_bounds
172+
assert np.allclose(model.theta, np.array([1.0, -1.0]))
173+
assert np.allclose(model.Lambda, np.array([-2.0]))
174+
assert model.negLnLike == pytest.approx(4.56)
175+
assert np.allclose(seen["x_likelihood"], np.array([1.0, -1.0, -2.0]))
176+
177+
178+
def test_fit_regression_with_optim_p_adds_p_bounds_and_sets_p(monkeypatch):
179+
X, y = _make_xy(k=2)
180+
model = Kriging(method="regression", optim_p=True, n_p=2, min_p=1.1, max_p=1.9)
181+
182+
seen = {"bounds": None}
183+
184+
def fake_max_likelihood(bounds):
185+
seen["bounds"] = bounds
186+
# k=2 thetas + 1 lambda + n_p=2 p-values
187+
return np.array([0.0, 0.2, -5.0, 1.3, 1.7], dtype=float), -10.0
188+
189+
def fake_likelihood(x):
190+
n = X.shape[0]
191+
return 0.5, np.eye(n), np.eye(n)
192+
193+
monkeypatch.setattr(model, "max_likelihood", fake_max_likelihood, raising=True)
194+
monkeypatch.setattr(model, "likelihood", fake_likelihood, raising=True)
195+
196+
model.fit(X, y)
197+
198+
expected_bounds = (
199+
[(model.min_theta, model.max_theta)] * X.shape[1]
200+
+ [(model.min_Lambda, model.max_Lambda)]
201+
+ [(model.min_p, model.max_p)] * model.n_p
202+
)
203+
assert seen["bounds"] == expected_bounds
204+
assert np.allclose(model.theta, np.array([0.0, 0.2]))
205+
assert np.allclose(model.Lambda, np.array([-5.0]))
206+
assert np.allclose(model.p_val, np.array([1.3, 1.7]))
207+
208+
209+
def test_fit_interpolation_with_optim_p_adds_p_bounds_and_sets_p(monkeypatch):
210+
X, y = _make_xy(k=2)
211+
model = Kriging(method="interpolation", optim_p=True, n_p=2, min_p=1.2, max_p=1.8)
212+
213+
seen = {"bounds": None}
214+
215+
def fake_max_likelihood(bounds):
216+
seen["bounds"] = bounds
217+
# k=2 thetas + n_p=2 p-values
218+
return np.array([0.3, -0.4, 1.25, 1.55], dtype=float), -3.0
219+
220+
def fake_likelihood(x):
221+
n = X.shape[0]
222+
return 1.0, np.eye(n), np.eye(n)
223+
224+
monkeypatch.setattr(model, "max_likelihood", fake_max_likelihood, raising=True)
225+
monkeypatch.setattr(model, "likelihood", fake_likelihood, raising=True)
226+
227+
model.fit(X, y)
228+
229+
expected_bounds = (
230+
[(model.min_theta, model.max_theta)] * X.shape[1]
231+
+ [(model.min_p, model.max_p)] * model.n_p
232+
)
233+
assert seen["bounds"] == expected_bounds
234+
assert model.Lambda is None
235+
assert np.allclose(model.theta, np.array([0.3, -0.4]))
236+
assert np.allclose(model.p_val, np.array([1.25, 1.55]))

0 commit comments

Comments
 (0)