Skip to content

Commit 68bfefc

Browse files
0.28.0 MCO
1 parent 047780b commit 68bfefc

11 files changed

Lines changed: 1318 additions & 322 deletions

File tree

notebooks/00_spotPython_tests.ipynb

Lines changed: 362 additions & 265 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.27.17"
10+
version = "0.28.0"
1111
authors = [
1212
{ name="T. Bartz-Beielstein", email="tbb@bartzundbartz.de" }
1313
]

src/spotpython/fun/hyperlight.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,21 @@ def check_X_shape(self, X: np.ndarray, fun_control: dict) -> np.ndarray:
8888

8989
def fun(self, X: np.ndarray, fun_control: dict = None) -> np.ndarray:
9090
"""
91-
Evaluates the function for the given input array X and control parameters.
91+
Evaluates the function for the given input array X of shape (n,k)
92+
and control parameters specified as a dict.
9293
Calls the train_model function from spotpython.light.trainmodel
9394
to train the model and evaluate the results.
9495
9596
Args:
9697
X (np.ndarray):
97-
input array.
98+
input array of shape (n, k) where n is the number of configurations evaluated
99+
and k is the number of hyperparameters.
98100
fun_control (dict):
99101
dictionary containing control parameters for the hyperparameter tuning.
100102
101103
Returns:
102104
(np.ndarray):
103-
array containing the evaluation results.
105+
(n,) array containing the `n` evaluation results.
104106
105107
Examples:
106108
>>> from math import inf
@@ -170,4 +172,6 @@ def fun(self, X: np.ndarray, fun_control: dict = None) -> np.ndarray:
170172
z_val = fun_control["weights"] * df_eval
171173
# Append, since several configurations can be evaluated at once.
172174
z_res = np.append(z_res, z_val)
175+
# Finally, z_res is a 1-dim array
176+
# of shape (n,) where n is the number of configurations evaluated.
173177
return z_res
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import numpy as np
2+
from numpy.random import default_rng
3+
from typing import List, Optional, Dict
4+
5+
6+
class MultiAnalytical:
7+
"""
8+
Class for multiobjective analytical test functions.
9+
10+
Args:
11+
offset (float):
12+
Offset value. Defaults to 0.0.
13+
seed (int):
14+
Seed value for random number generation. Defaults to 126.
15+
fun_control (dict):
16+
Dictionary containing control parameters for the function. Defaults to None.
17+
18+
Notes:
19+
See [Numpy Random Sampling](https://numpy.org/doc/stable/reference/random/index.html#random-quick-start)
20+
21+
Attributes:
22+
offset (float):
23+
Offset value.
24+
sigma (float):
25+
Noise level.
26+
seed (int):
27+
Seed value for random number generation.
28+
rng (Generator):
29+
Numpy random number generator object.
30+
fun_control (dict):
31+
Dictionary containing control parameters for the function.
32+
m (int):
33+
Number of objectives.
34+
"""
35+
36+
def __init__(self, offset: float = 0.0, sigma=0.0, seed: int = 126, fun_control=None, m=1) -> None:
37+
self.offset = offset
38+
self.sigma = sigma
39+
self.m = m
40+
self.seed = seed
41+
self.rng = default_rng(seed=self.seed)
42+
self.fun_control = {"offset": offset, "sigma": self.sigma, "seed": self.seed}
43+
# overwrite fun_control with user input if provided
44+
if fun_control is not None:
45+
self.fun_control = fun_control
46+
# check if fun_control contains offset, sigma and seed, if not, add them
47+
if "offset" not in self.fun_control:
48+
self.fun_control["offset"] = self.offset
49+
if "sigma" not in self.fun_control:
50+
self.fun_control["sigma"] = self.sigma
51+
if "seed" not in self.fun_control:
52+
self.fun_control["seed"] = self.seed
53+
54+
def __repr__(self) -> str:
55+
return f"analytical(offset={self.offset}, sigma={self.sigma}, seed={self.seed})"
56+
57+
def _prepare_input_data(self, X, fun_control):
58+
if fun_control is not None:
59+
self.fun_control = fun_control
60+
if not isinstance(X, np.ndarray):
61+
X = np.array(X)
62+
X = np.atleast_2d(X)
63+
return X
64+
65+
def _add_noise(self, y: List[float]) -> np.ndarray:
66+
"""
67+
Adds noise to the input data.
68+
This method takes in a list of float values y as input and adds noise to
69+
the data using a random number generator. The method returns a numpy array
70+
containing the noisy data.
71+
72+
Args:
73+
self (analytical): analytical class object.
74+
y (List[float]): Input data.
75+
76+
Returns:
77+
np.ndarray: Noisy data.
78+
79+
"""
80+
if self.fun_control["sigma"] > 0:
81+
# Use own rng:
82+
if self.fun_control["seed"] is not None:
83+
rng = default_rng(seed=self.fun_control["seed"])
84+
# Use class rng:
85+
else:
86+
rng = self.rng
87+
noise_y = np.array([], dtype=float)
88+
for y_i in y:
89+
noise_y = np.append(
90+
noise_y,
91+
y_i + rng.normal(loc=0, scale=self.fun_control["sigma"], size=1),
92+
)
93+
return noise_y
94+
else:
95+
return y
96+
97+
def fun_mo_linear(self, X: np.ndarray, fun_control: Optional[Dict] = None) -> np.ndarray:
98+
"""Linear function with multi-objective support.
99+
100+
Args:
101+
X (np.ndarray): Input array of shape (n, k), where n is the number of samples and k is the number of features.
102+
fun_control (dict): Dictionary with entries `sigma` (noise level) and `seed` (random seed).
103+
104+
Returns:
105+
np.ndarray: A 2D numpy array with shape (n, m), where n is the number of samples and m is the number of objectives.
106+
107+
Examples:
108+
>>> from spotpython.fun.multiobjectivefunctions import MultiAnalytical
109+
import numpy as np
110+
fun = MultiAnalytical(m=1)
111+
# Input data
112+
X = np.array([[0, 0, 0], [1, 1, 1]])
113+
# Single objective
114+
print(fun.fun_mo_linear(X))
115+
# Output: [[0.]
116+
# [3.]]
117+
# Two objectives
118+
fun = MultiAnalytical(m=2)
119+
print(fun.fun_mo_linear(X))
120+
# Output: [[ 0. -0.]
121+
# [ 3. -3.]]
122+
# Three objectives
123+
fun = MultiAnalytical(m=3)
124+
print(fun.fun_mo_linear(X))
125+
# Output: [[ 0. -0. 0.]
126+
# [ 3. -3. 3.]]
127+
# Four objectives
128+
fun = MultiAnalytical(m=4)
129+
print(fun.fun_mo_linear(X))
130+
# Output: [[ 0. -0. 0. -0.]
131+
# [ 3. -3. 3. -3.]]
132+
"""
133+
X = self._prepare_input_data(X, fun_control)
134+
offset = np.ones(X.shape[1]) * self.offset
135+
136+
alpha = self.fun_control.get("alpha", 0.0)
137+
beta = self.fun_control.get("beta", None)
138+
if beta is not None:
139+
# Check if beta is a numpy array
140+
if not isinstance(beta, np.ndarray):
141+
# Convert beta to numpy array of shape (n,), where n is the number of columns in X
142+
beta = np.array(beta)
143+
if beta.shape[0] != X.shape[1]:
144+
raise Exception("beta must have the same number of elements as the number of columns in X")
145+
146+
# Compute the linear response
147+
if beta is not None:
148+
# Weighted sum with intercept
149+
y_0 = alpha + np.dot(X - offset, beta)
150+
else:
151+
# Original behavior: just sum the rows
152+
y_0 = alpha + np.sum(X - offset, axis=1)
153+
154+
# Add noise to the primary objective
155+
y_0 = self._add_noise(y_0)
156+
157+
# Generate multi-objective outputs
158+
objectives = [y_0 if i % 2 == 0 else -y_0 for i in range(self.m)]
159+
return np.column_stack(objectives)

src/spotpython/spot/spot.py

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,7 @@ def run(self, X_start: np.ndarray = None) -> Spot:
792792
793793
Args:
794794
X_start (numpy.ndarray, optional): initial design. Defaults to None.
795+
The initial design must have shape (n, k), where n is the number of points and k is the number of dimensions.
795796
796797
Returns:
797798
Spot: The `Spot` instance configured and updated based on the optimization process.
@@ -906,6 +907,7 @@ def initialize_design(self, X_start=None) -> None:
906907
907908
Args:
908909
X_start (numpy.ndarray, optional): initial design. Defaults to None.
910+
Must be of shape (n, k), where n is the number of points and k is the number of dimensions.
909911
910912
Attributes:
911913
self.X (numpy.ndarray): initial design
@@ -965,7 +967,7 @@ def initialize_design_matrix(self, X_start=None) -> None:
965967
966968
Args:
967969
X_start (numpy.ndarray, optional): User-provided starting points
968-
for the design. Shape should be (n_samples, n_features).
970+
for the design. Shape should be (n=n_samples, k=n_features).
969971
Defaults to None.
970972
971973
Returns:
@@ -975,7 +977,7 @@ def initialize_design_matrix(self, X_start=None) -> None:
975977
Raises:
976978
Exception: If the resulting design matrix has zero rows.
977979
978-
Note:
980+
Notes:
979981
* If `X_start` is not in the expected shape, it is ignored.
980982
981983
Examples:
@@ -1035,15 +1037,19 @@ def initialize_design_matrix(self, X_start=None) -> None:
10351037
self.X = repair_non_numeric(X0, self.var_type)
10361038

10371039
def _store_mo(self, y_mo) -> None:
1038-
# store y_mo in self.y_mo (append new values)
1039-
if self.y_mo is None:
1040-
self.y_mo = np.atleast_2d(y_mo)
1040+
# store y_mo in self.y_mo (append new values) if mo, otherwise self.y_mo is None
1041+
if self.y_mo is None and y_mo.ndim == 2:
1042+
self.y_mo = y_mo
10411043
else: # append new values
1042-
print(f"y_mo: {y_mo}")
1043-
print(f"self.y_mo: {self.y_mo}")
1044-
print(f"y_mo.shape: {y_mo.shape}")
1045-
print(f"self.y_mo.shape: {self.y_mo.shape}")
1046-
self.y_mo = np.concatenate((self.y_mo, y_mo), axis=1)
1044+
# before stacking the arrays, check if the number of columns is the same in the mo case
1045+
if y_mo.ndim == 2 and self.y_mo.ndim == 2:
1046+
if self.y_mo.shape[1] != y_mo.shape[1]:
1047+
print(f"Shape of y_mo: {y_mo.shape}")
1048+
print(f"y_mo: {y_mo}")
1049+
print(f"Shape of self.y_mo: {self.y_mo.shape}")
1050+
print(f"self.y_mo: {self.y_mo}")
1051+
raise ValueError(f"Number of columns (objectives) in y_mo ({y_mo.shape[1]}) " f"does not match the number of columns in self.y_mo ({self.y_mo.shape[1]})")
1052+
self.y_mo = np.vstack((self.y_mo, y_mo))
10471053

10481054
def _mo2so(self, y_mo) -> None:
10491055
"""
@@ -1056,33 +1062,28 @@ def _mo2so(self, y_mo) -> None:
10561062
10571063
Args:
10581064
y_mo (numpy.ndarray):
1059-
A 2D array of shape (m, n), where ``m`` is
1060-
the number of objectives and ``n`` is the number of data points.
1061-
1065+
If multi-objective values are present, this is an array of shape (n, m), where ``m`` is
1066+
the number of objectives and ``n`` is the number of data points.
1067+
Otherwise, it is an array of shape (n,) with single-objective values.
10621068
Returns:
10631069
numpy.ndarray:
1064-
A 1D array of shape (n,) with single-objective values if ``m > 1``. If only one
1065-
objective is present (``m == 1``), no transformation is performed.
1070+
A 1D array of shape (n,) with single-objective values.
10661071
10671072
"""
1068-
n, k = get_shape(y_mo)
1069-
# Ensure that y_mo is a (n, k) numpy array
1070-
y_mo = np.atleast_2d(y_mo)
1071-
# TODO
1072-
# self._store_mo(y_mo)
1073-
m = y_mo.shape[0] # Number of objectives
1074-
if m > 1:
1073+
n, m = get_shape(y_mo)
1074+
self._store_mo(y_mo)
1075+
# do not use m as a condition, because m can be None, use ndim instead
1076+
if y_mo.ndim == 2:
10751077
if self.fun_control["fun_mo2so"] is not None:
10761078
y0 = self.fun_control["fun_mo2so"](y_mo)
10771079
else:
1078-
# Select the first row of an (m, k) array
1079-
y0 = y_mo[0, :]
1080+
# Select the first column of an (n,m) array
1081+
if y_mo.size > 0:
1082+
y0 = y_mo[:, 0]
1083+
else:
1084+
y0 = y_mo
10801085
else:
1081-
if k is None:
1082-
y0 = y_mo.flatten()
1083-
else:
1084-
y0 = y_mo # Keep as 2D array for single-objective case
1085-
1086+
y0 = y_mo
10861087
return y0
10871088

10881089
def evaluate_initial_design(self) -> None:
@@ -1138,6 +1139,9 @@ def evaluate_initial_design(self) -> None:
11381139
logger.debug("In Spot() evaluate_initial_design(), before calling self.fun: fun_control: %s", self.fun_control)
11391140

11401141
y_mo = self.fun(X=X_all, fun_control=self.fun_control)
1142+
if self.verbosity > 1:
1143+
print(f"y_mo as returned from fun(): {y_mo}")
1144+
print(f"y_mo shape: {y_mo.shape}")
11411145

11421146
# Convert multi-objective values to single-objective values
11431147
# TODO: Store y_mo in self.y_mo (append new values)
@@ -1470,9 +1474,8 @@ def update_design(self) -> None:
14701474
# (S-18): Evaluating New Solutions:
14711475
y_mo = self.fun(X=X_all, fun_control=self.fun_control)
14721476
# Convert multi-objective values to single-objective values:
1473-
# TODO: Store y_mo in self.y_mo (append new values)
14741477
y0 = self._mo2so(y_mo)
1475-
1478+
# Apply penalty for NA values works only on so values:
14761479
y0 = apply_penalty_NA(y0, self.fun_control["penalty_NA"], verbosity=self.verbosity)
14771480
X0, y0 = remove_nan(X0, y0, stop_on_zero_return=False)
14781481
# Append New Solutions (only if they are not nan):
@@ -1709,6 +1712,7 @@ def generate_random_point(self):
17091712
# TODO: Store y_mo in self.y_mo (append new values)
17101713
y_mo = self.fun(X=X_all, fun_control=self.fun_control)
17111714
y0 = self._mo2so(y_mo)
1715+
# Apply penalty for NA values works only on so values:
17121716
y0 = apply_penalty_NA(y0, self.fun_control["penalty_NA"], verbosity=self.verbosity)
17131717
X0, y0 = remove_nan(X0, y0, stop_on_zero_return=False)
17141718
return X0, y0
@@ -2045,8 +2049,8 @@ def plot_model(self, y_min=None, y_max=None) -> None:
20452049
X_test = np.linspace(self.lower[0], self.upper[0], 100)
20462050
y_mo = self.fun(X=X_test.reshape(-1, 1), fun_control=self.fun_control)
20472051
# convert multi-objective values to single-objective values
2048-
# TODO: Store y_mo in self.y_mo (append new values)
20492052
y_test = self._mo2so(y_mo)
2053+
# Apply penalty for NA values works only on so values:
20502054
y_test = apply_penalty_NA(y_test, self.fun_control["penalty_NA"], verbosity=self.verbosity)
20512055
if isinstance(self.surrogate, Kriging):
20522056
y_hat = self.surrogate.predict(X_test[:, np.newaxis], return_val="y")

0 commit comments

Comments
 (0)