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.

@@ -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
+ )
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from spotoptim!"
File without changes