spotoptim 0.0.2__tar.gz
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.
Potentially problematic release.
This version of spotoptim might be problematic. Click here for more details.
spotoptim-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: spotoptim
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: bartzbeielstein
|
|
6
|
+
Author-email: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com>
|
|
7
|
+
Requires-Dist: fastapi>=0.121.1
|
|
8
|
+
Requires-Dist: numpy>=1.24.3
|
|
9
|
+
Requires-Dist: scipy>=1.10.1
|
|
10
|
+
Requires-Dist: uvicorn>=0.22.0
|
|
11
|
+
Requires-Dist: scikit-learn
|
|
12
|
+
Requires-Dist: spotpython
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "spotoptim"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "bartzbeielstein", email = "32470350+bartzbeielstein@users.noreply.github.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fastapi>=0.121.1",
|
|
12
|
+
"numpy>=1.24.3",
|
|
13
|
+
"scipy>=1.10.1",
|
|
14
|
+
"uvicorn>=0.22.0",
|
|
15
|
+
"scikit-learn",
|
|
16
|
+
"spotpython"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.9.8,<0.10.0"]
|
|
21
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Callable, Optional, Tuple
|
|
3
|
+
from scipy.optimize import OptimizeResult, differential_evolution
|
|
4
|
+
from sklearn.base import BaseEstimator
|
|
5
|
+
from sklearn.gaussian_process import GaussianProcessRegressor
|
|
6
|
+
from sklearn.gaussian_process.kernels import Matern, ConstantKernel
|
|
7
|
+
from spotpython.design.spacefilling import SpaceFilling
|
|
8
|
+
from spotpython.utils.repair import repair_non_numeric
|
|
9
|
+
from spotpython.utils.compare import selectNew
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SpotOptim(BaseEstimator):
|
|
13
|
+
"""
|
|
14
|
+
SPOT optimizer compatible with scipy.optimize interface.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
fun : callable
|
|
19
|
+
Objective function to minimize. Should accept array of shape (n_samples, n_features).
|
|
20
|
+
bounds : list of tuple
|
|
21
|
+
Bounds for each dimension as [(low, high), ...].
|
|
22
|
+
max_iter : int, default=20
|
|
23
|
+
Maximum number of optimization iterations.
|
|
24
|
+
n_initial : int, default=10
|
|
25
|
+
Number of initial design points.
|
|
26
|
+
surrogate : object, optional
|
|
27
|
+
Surrogate model (default: Gaussian Process with Matern kernel).
|
|
28
|
+
acquisition : str, default='ei'
|
|
29
|
+
Acquisition function ('ei', 'y', 'pi').
|
|
30
|
+
var_type : list of str, optional
|
|
31
|
+
Variable types for each dimension ('num', 'int', 'float', 'factor').
|
|
32
|
+
tolerance_x : float, default=1e-6
|
|
33
|
+
Minimum distance between points.
|
|
34
|
+
seed : int, optional
|
|
35
|
+
Random seed for reproducibility.
|
|
36
|
+
verbose : bool, default=False
|
|
37
|
+
Print progress information.
|
|
38
|
+
|
|
39
|
+
Attributes
|
|
40
|
+
----------
|
|
41
|
+
X_ : ndarray of shape (n_samples, n_features)
|
|
42
|
+
All evaluated points.
|
|
43
|
+
y_ : ndarray of shape (n_samples,)
|
|
44
|
+
Function values at X_.
|
|
45
|
+
best_x_ : ndarray of shape (n_features,)
|
|
46
|
+
Best point found.
|
|
47
|
+
best_y_ : float
|
|
48
|
+
Best function value found.
|
|
49
|
+
n_iter_ : int
|
|
50
|
+
Number of iterations performed.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
>>> def objective(X):
|
|
55
|
+
... return np.sum(X**2, axis=1)
|
|
56
|
+
...
|
|
57
|
+
>>> bounds = [(-5, 5), (-5, 5)]
|
|
58
|
+
>>> optimizer = SpotOptim(fun=objective, bounds=bounds, max_iter=10, n_initial=5, verbose=True)
|
|
59
|
+
>>> result = optimizer.optimize()
|
|
60
|
+
>>> print("Best x:", result.x)
|
|
61
|
+
>>> print("Best f(x):", result.fun)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
fun: Callable,
|
|
67
|
+
bounds: list,
|
|
68
|
+
max_iter: int = 20,
|
|
69
|
+
n_initial: int = 10,
|
|
70
|
+
surrogate: Optional[object] = None,
|
|
71
|
+
acquisition: str = "ei",
|
|
72
|
+
var_type: Optional[list] = None,
|
|
73
|
+
tolerance_x: float = 1e-6,
|
|
74
|
+
seed: Optional[int] = None,
|
|
75
|
+
verbose: bool = False,
|
|
76
|
+
):
|
|
77
|
+
self.fun = fun
|
|
78
|
+
self.bounds = bounds
|
|
79
|
+
self.max_iter = max_iter
|
|
80
|
+
self.n_initial = n_initial
|
|
81
|
+
self.surrogate = surrogate
|
|
82
|
+
self.acquisition = acquisition
|
|
83
|
+
self.var_type = var_type
|
|
84
|
+
self.tolerance_x = tolerance_x
|
|
85
|
+
self.seed = seed
|
|
86
|
+
self.verbose = verbose
|
|
87
|
+
|
|
88
|
+
# Derived attributes
|
|
89
|
+
self.n_dim = len(bounds)
|
|
90
|
+
self.lower = np.array([b[0] for b in bounds])
|
|
91
|
+
self.upper = np.array([b[1] for b in bounds])
|
|
92
|
+
|
|
93
|
+
# Default variable types
|
|
94
|
+
if self.var_type is None:
|
|
95
|
+
self.var_type = ["num"] * self.n_dim
|
|
96
|
+
|
|
97
|
+
# Initialize surrogate if not provided
|
|
98
|
+
if self.surrogate is None:
|
|
99
|
+
kernel = ConstantKernel(1.0, (1e-3, 1e3)) * Matern(
|
|
100
|
+
length_scale=1.0, length_scale_bounds=(1e-2, 1e2), nu=2.5
|
|
101
|
+
)
|
|
102
|
+
self.surrogate = GaussianProcessRegressor(
|
|
103
|
+
kernel=kernel,
|
|
104
|
+
n_restarts_optimizer=10,
|
|
105
|
+
normalize_y=True,
|
|
106
|
+
random_state=self.seed,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Design generator
|
|
110
|
+
self.design = SpaceFilling(k=self.n_dim, seed=self.seed)
|
|
111
|
+
|
|
112
|
+
# Storage for results
|
|
113
|
+
self.X_ = None
|
|
114
|
+
self.y_ = None
|
|
115
|
+
self.best_x_ = None
|
|
116
|
+
self.best_y_ = None
|
|
117
|
+
self.n_iter_ = 0
|
|
118
|
+
|
|
119
|
+
def _evaluate_function(self, X: np.ndarray) -> np.ndarray:
|
|
120
|
+
"""Evaluate objective function at points X."""
|
|
121
|
+
# Ensure X is 2D
|
|
122
|
+
X = np.atleast_2d(X)
|
|
123
|
+
|
|
124
|
+
# Evaluate function
|
|
125
|
+
y = self.fun(X)
|
|
126
|
+
|
|
127
|
+
# Ensure y is 1D
|
|
128
|
+
if isinstance(y, np.ndarray) and y.ndim > 1:
|
|
129
|
+
y = y.ravel()
|
|
130
|
+
elif not isinstance(y, np.ndarray):
|
|
131
|
+
y = np.array([y])
|
|
132
|
+
|
|
133
|
+
return y
|
|
134
|
+
|
|
135
|
+
def _generate_initial_design(self) -> np.ndarray:
|
|
136
|
+
"""Generate initial space-filling design."""
|
|
137
|
+
X0 = self.design.scipy_lhd(
|
|
138
|
+
n=self.n_initial, repeats=1, lower=self.lower, upper=self.upper
|
|
139
|
+
)
|
|
140
|
+
return repair_non_numeric(X0, self.var_type)
|
|
141
|
+
|
|
142
|
+
def _fit_surrogate(self, X: np.ndarray, y: np.ndarray) -> None:
|
|
143
|
+
"""Fit surrogate model to data."""
|
|
144
|
+
self.surrogate.fit(X, y)
|
|
145
|
+
|
|
146
|
+
def _acquisition_function(self, x: np.ndarray) -> float:
|
|
147
|
+
"""
|
|
148
|
+
Compute acquisition function value.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
x : ndarray of shape (n_features,)
|
|
153
|
+
Point to evaluate.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
float
|
|
158
|
+
Acquisition function value (to be minimized).
|
|
159
|
+
"""
|
|
160
|
+
x = x.reshape(1, -1)
|
|
161
|
+
|
|
162
|
+
if self.acquisition == "y":
|
|
163
|
+
# Predicted mean
|
|
164
|
+
return self.surrogate.predict(x)[0]
|
|
165
|
+
|
|
166
|
+
elif self.acquisition == "ei":
|
|
167
|
+
# Expected Improvement
|
|
168
|
+
mu, sigma = self.surrogate.predict(x, return_std=True)
|
|
169
|
+
mu = mu[0]
|
|
170
|
+
sigma = sigma[0]
|
|
171
|
+
|
|
172
|
+
if sigma < 1e-10:
|
|
173
|
+
return 0.0
|
|
174
|
+
|
|
175
|
+
y_best = np.min(self.y_)
|
|
176
|
+
improvement = y_best - mu
|
|
177
|
+
Z = improvement / sigma
|
|
178
|
+
|
|
179
|
+
from scipy.stats import norm
|
|
180
|
+
|
|
181
|
+
ei = improvement * norm.cdf(Z) + sigma * norm.pdf(Z)
|
|
182
|
+
return -ei # Minimize negative EI
|
|
183
|
+
|
|
184
|
+
elif self.acquisition == "pi":
|
|
185
|
+
# Probability of Improvement
|
|
186
|
+
mu, sigma = self.surrogate.predict(x, return_std=True)
|
|
187
|
+
mu = mu[0]
|
|
188
|
+
sigma = sigma[0]
|
|
189
|
+
|
|
190
|
+
if sigma < 1e-10:
|
|
191
|
+
return 0.0
|
|
192
|
+
|
|
193
|
+
y_best = np.min(self.y_)
|
|
194
|
+
Z = (y_best - mu) / sigma
|
|
195
|
+
|
|
196
|
+
from scipy.stats import norm
|
|
197
|
+
|
|
198
|
+
pi = norm.cdf(Z)
|
|
199
|
+
return -pi # Minimize negative PI
|
|
200
|
+
|
|
201
|
+
else:
|
|
202
|
+
raise ValueError(f"Unknown acquisition function: {self.acquisition}")
|
|
203
|
+
|
|
204
|
+
def _suggest_next_point(self) -> np.ndarray:
|
|
205
|
+
"""
|
|
206
|
+
Suggest next point to evaluate using acquisition function optimization.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
ndarray of shape (n_features,)
|
|
211
|
+
Next point to evaluate.
|
|
212
|
+
"""
|
|
213
|
+
result = differential_evolution(
|
|
214
|
+
func=self._acquisition_function,
|
|
215
|
+
bounds=self.bounds,
|
|
216
|
+
seed=self.seed,
|
|
217
|
+
maxiter=1000,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
x_next = result.x
|
|
221
|
+
|
|
222
|
+
# Ensure minimum distance to existing points
|
|
223
|
+
x_next_2d = x_next.reshape(1, -1)
|
|
224
|
+
x_new, _ = selectNew(A=x_next_2d, X=self.X_, tolerance=self.tolerance_x)
|
|
225
|
+
|
|
226
|
+
if x_new.shape[0] == 0:
|
|
227
|
+
# If too close, generate random point
|
|
228
|
+
if self.verbose:
|
|
229
|
+
print("Proposed point too close, generating random point")
|
|
230
|
+
x_next = self.design.scipy_lhd(
|
|
231
|
+
n=1, repeats=1, lower=self.lower, upper=self.upper
|
|
232
|
+
)[0]
|
|
233
|
+
|
|
234
|
+
return repair_non_numeric(x_next.reshape(1, -1), self.var_type)[0]
|
|
235
|
+
|
|
236
|
+
def optimize(self, X0: Optional[np.ndarray] = None) -> OptimizeResult:
|
|
237
|
+
"""
|
|
238
|
+
Run the optimization process.
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
X0 : ndarray of shape (n_initial, n_features), optional
|
|
243
|
+
Initial design points. If None, generates space-filling design.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
OptimizeResult
|
|
248
|
+
Optimization result with fields:
|
|
249
|
+
- x : best point found
|
|
250
|
+
- fun : best function value
|
|
251
|
+
- nfev : number of function evaluations
|
|
252
|
+
- success : whether optimization succeeded
|
|
253
|
+
- message : termination message
|
|
254
|
+
- X : all evaluated points
|
|
255
|
+
- y : all function values
|
|
256
|
+
"""
|
|
257
|
+
# Generate or use provided initial design
|
|
258
|
+
if X0 is None:
|
|
259
|
+
X0 = self._generate_initial_design()
|
|
260
|
+
else:
|
|
261
|
+
X0 = np.atleast_2d(X0)
|
|
262
|
+
X0 = repair_non_numeric(X0, self.var_type)
|
|
263
|
+
|
|
264
|
+
# Evaluate initial design
|
|
265
|
+
y0 = self._evaluate_function(X0)
|
|
266
|
+
|
|
267
|
+
# Initialize storage
|
|
268
|
+
self.X_ = X0.copy()
|
|
269
|
+
self.y_ = y0.copy()
|
|
270
|
+
self.n_iter_ = 0
|
|
271
|
+
|
|
272
|
+
# Initial best
|
|
273
|
+
best_idx = np.argmin(self.y_)
|
|
274
|
+
self.best_x_ = self.X_[best_idx].copy()
|
|
275
|
+
self.best_y_ = self.y_[best_idx]
|
|
276
|
+
|
|
277
|
+
if self.verbose:
|
|
278
|
+
print(f"Initial best: f(x) = {self.best_y_:.6f}")
|
|
279
|
+
|
|
280
|
+
# Main optimization loop
|
|
281
|
+
for iteration in range(self.max_iter):
|
|
282
|
+
self.n_iter_ = iteration + 1
|
|
283
|
+
|
|
284
|
+
# Fit surrogate
|
|
285
|
+
self._fit_surrogate(self.X_, self.y_)
|
|
286
|
+
|
|
287
|
+
# Suggest next point
|
|
288
|
+
x_next = self._suggest_next_point()
|
|
289
|
+
|
|
290
|
+
# Evaluate next point
|
|
291
|
+
y_next = self._evaluate_function(x_next.reshape(1, -1))
|
|
292
|
+
|
|
293
|
+
# Update storage
|
|
294
|
+
self.X_ = np.vstack([self.X_, x_next])
|
|
295
|
+
self.y_ = np.append(self.y_, y_next)
|
|
296
|
+
|
|
297
|
+
# Update best
|
|
298
|
+
if y_next[0] < self.best_y_:
|
|
299
|
+
self.best_x_ = x_next.copy()
|
|
300
|
+
self.best_y_ = y_next[0]
|
|
301
|
+
|
|
302
|
+
if self.verbose:
|
|
303
|
+
print(
|
|
304
|
+
f"Iteration {iteration+1}: New best f(x) = {self.best_y_:.6f}"
|
|
305
|
+
)
|
|
306
|
+
elif self.verbose:
|
|
307
|
+
print(f"Iteration {iteration+1}: f(x) = {y_next[0]:.6f}")
|
|
308
|
+
|
|
309
|
+
# Return scipy-style result
|
|
310
|
+
return OptimizeResult(
|
|
311
|
+
x=self.best_x_,
|
|
312
|
+
fun=self.best_y_,
|
|
313
|
+
nfev=len(self.y_),
|
|
314
|
+
nit=self.n_iter_,
|
|
315
|
+
success=True,
|
|
316
|
+
message="Optimization finished successfully",
|
|
317
|
+
X=self.X_,
|
|
318
|
+
y=self.y_,
|
|
319
|
+
)
|
|
File without changes
|