Skip to content

Commit 83a5d2d

Browse files
0.27.0 mco
1 parent 4094650 commit 83a5d2d

6 files changed

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

src/spotpython/spot/spot.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from numpy import spacing
2727
from numpy import append
2828
from numpy import min, max
29+
from spotpython.utils.convert import get_shape, set_shape
2930
from spotpython.utils.init import fun_control_init, optimizer_control_init, surrogate_control_init, design_control_init
3031
from spotpython.utils.compare import selectNew
3132
from spotpython.utils.aggregate import aggregate_mean_var, select_distant_points
@@ -1032,6 +1033,44 @@ def initialize_design_matrix(self, X_start=None) -> None:
10321033

10331034
self.X = repair_non_numeric(X0, self.var_type)
10341035

1036+
def _mo2so(self, y_mo) -> None:
1037+
"""
1038+
Converts multi-objective values to a single-objective value by applying a user-defined
1039+
function from ``fun_control['fun_mo2so']``. If no user-defined function is given, the
1040+
values in the first objective row are used.
1041+
1042+
This method is called after the objective function evaluation (i.e., after ``self.fun()``).
1043+
It typically returns a 1D array with the single-objective values.
1044+
1045+
Args:
1046+
y_mo (numpy.ndarray):
1047+
A 2D array of shape (m, n), where ``m`` is
1048+
the number of objectives and ``n`` is the number of data points.
1049+
1050+
Returns:
1051+
numpy.ndarray:
1052+
A 1D array of shape (n,) with single-objective values if ``m > 1``. If only one
1053+
objective is present (``m == 1``), no transformation is performed.
1054+
1055+
"""
1056+
n, k = get_shape(y_mo)
1057+
# Ensure that y_mo is a (n, k) numpy array
1058+
y_mo = np.atleast_2d(y_mo)
1059+
m = y_mo.shape[0] # Number of objectives
1060+
if m > 1:
1061+
if self.fun_control["fun_mo2so"] is not None:
1062+
y0 = self.fun_control["fun_mo2so"](y_mo)
1063+
else:
1064+
# Select the first row of an (m, k) array
1065+
y0 = y_mo[0, :]
1066+
else:
1067+
if k is None:
1068+
y0 = y_mo.flatten()
1069+
else:
1070+
y0 = y_mo # Keep as 2D array for single-objective case
1071+
1072+
return y0
1073+
10351074
def evaluate_initial_design(self) -> None:
10361075
"""
10371076
Evaluate the initial design.
@@ -1084,7 +1123,10 @@ def evaluate_initial_design(self) -> None:
10841123
logger.debug("In Spot() evaluate_initial_design(), before calling self.fun: X_all: %s", X_all)
10851124
logger.debug("In Spot() evaluate_initial_design(), before calling self.fun: fun_control: %s", self.fun_control)
10861125

1087-
self.y = self.fun(X=X_all, fun_control=self.fun_control)
1126+
y_mo = self.fun(X=X_all, fun_control=self.fun_control)
1127+
# Convert multi-objective values to single-objective values
1128+
# TODO: Store y_mo in self.y_mo (append new values)
1129+
self.y = self._mo2so(y_mo)
10881130
self.y = apply_penalty_NA(self.y, self.fun_control["penalty_NA"], verbosity=self.verbosity)
10891131
logger.debug("In Spot() evaluate_initial_design(), after calling self.fun: self.y: %s", self.y)
10901132

@@ -1411,7 +1453,11 @@ def update_design(self) -> None:
14111453
self.fun_control["seed"],
14121454
)
14131455
# (S-18): Evaluating New Solutions:
1414-
y0 = self.fun(X=X_all, fun_control=self.fun_control)
1456+
y_mo = self.fun(X=X_all, fun_control=self.fun_control)
1457+
# Convert multi-objective values to single-objective values:
1458+
# TODO: Store y_mo in self.y_mo (append new values)
1459+
y0 = self._mo2so(y_mo)
1460+
14151461
y0 = apply_penalty_NA(y0, self.fun_control["penalty_NA"], verbosity=self.verbosity)
14161462
X0, y0 = remove_nan(X0, y0, stop_on_zero_return=False)
14171463
# Append New Solutions (only if they are not nan):
@@ -1644,7 +1690,10 @@ def generate_random_point(self):
16441690
X_all = self.to_all_dim_if_needed(X0)
16451691
logger.debug("In Spot() generate_random_point(), before calling self.fun: X_all: %s", X_all)
16461692
logger.debug("In Spot() generate_random_point(), before calling self.fun: fun_control: %s", self.fun_control)
1647-
y0 = self.fun(X=X_all, fun_control=self.fun_control)
1693+
# Convert multi-objective values to single-objective values
1694+
# TODO: Store y_mo in self.y_mo (append new values)
1695+
y_mo = self.fun(X=X_all, fun_control=self.fun_control)
1696+
y0 = self._mo2so(y_mo)
16481697
y0 = apply_penalty_NA(y0, self.fun_control["penalty_NA"], verbosity=self.verbosity)
16491698
X0, y0 = remove_nan(X0, y0, stop_on_zero_return=False)
16501699
return X0, y0
@@ -1979,7 +2028,10 @@ def plot_model(self, y_min=None, y_max=None) -> None:
19792028
"""
19802029
if self.k == 1:
19812030
X_test = np.linspace(self.lower[0], self.upper[0], 100)
1982-
y_test = self.fun(X=X_test.reshape(-1, 1), fun_control=self.fun_control)
2031+
y_mo = self.fun(X=X_test.reshape(-1, 1), fun_control=self.fun_control)
2032+
# convert multi-objective values to single-objective values
2033+
# TODO: Store y_mo in self.y_mo (append new values)
2034+
y_test = self._mo2so(y_mo)
19832035
y_test = apply_penalty_NA(y_test, self.fun_control["penalty_NA"], verbosity=self.verbosity)
19842036
if isinstance(self.surrogate, Kriging):
19852037
y_hat = self.surrogate.predict(X_test[:, np.newaxis], return_val="y")

src/spotpython/utils/convert.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,85 @@ def set_dataset_target_type(dataset, target="y") -> pd.DataFrame:
247247
# convert the target column to 0 and 1
248248
dataset[target] = dataset[target].astype(int)
249249
return dataset
250+
251+
252+
def get_shape(x: np.ndarray) -> tuple:
253+
"""
254+
Get the shape of a numpy array `x`.
255+
256+
This function returns the number of rows and columns of the input array `x`.
257+
If `x` is a 1D array (shape `(n,)`), it returns `(n, None)`.
258+
If `x` is a 2D array (shape `(n, k)`), it returns `(n, k)`.
259+
260+
Args:
261+
x (numpy.ndarray): The input numpy array.
262+
263+
Returns:
264+
tuple: A tuple `(n, k)` where:
265+
- `n` is the number of rows in the array.
266+
- `k` is the number of columns in the array, or `None` if `x` is 1D.
267+
268+
Examples:
269+
>>> import numpy as np
270+
>>> from spotpython.utils.convert import get_shape
271+
>>> x1 = np.array([1, 2, 3])
272+
>>> get_shape(x1)
273+
(3, None)
274+
275+
>>> x2 = np.array([[1, 2], [3, 4], [5, 6]])
276+
>>> get_shape(x2)
277+
(3, 2)
278+
"""
279+
if x.ndim == 1:
280+
return x.shape[0], None
281+
elif x.ndim == 2:
282+
return x.shape[0], x.shape[1]
283+
else:
284+
raise ValueError("Input array must be 1D or 2D.")
285+
286+
287+
def set_shape(x: np.ndarray, target_shape: tuple) -> np.ndarray:
288+
"""
289+
Adjust the shape of a numpy array `x` to match the target shape `(n, k)`.
290+
291+
If the target shape is `(n, None)`, the array is reshaped to 1D with `n` elements.
292+
If the target shape is `(n, k)`, the array is reshaped to 2D with `n` rows and `k` columns.
293+
294+
Args:
295+
x (numpy.ndarray): The input numpy array.
296+
target_shape (tuple): The target shape `(n, k)` where:
297+
- `n` is the number of rows.
298+
- `k` is the number of columns, or `None` for a 1D array.
299+
300+
Returns:
301+
numpy.ndarray: The reshaped numpy array.
302+
303+
Raises:
304+
ValueError: If the target shape is incompatible with the size of the array.
305+
306+
Examples:
307+
>>> import numpy as np
308+
>>> from spotpython.utils.convert import set_shape
309+
>>> x = np.array([1, 2, 3, 4])
310+
>>> set_shape(x, (4, None))
311+
array([1, 2, 3, 4])
312+
>>> x = np.array([1, 2, 3, 4])
313+
>>> set_shape(x, (2, 2))
314+
array([[1, 2],
315+
[3, 4]])
316+
>>> x = np.array([[1, 2], [3, 4]])
317+
>>> set_shape(x, (4, None))
318+
array([1, 2, 3, 4])
319+
"""
320+
n, k = target_shape
321+
322+
if k is None:
323+
# Reshape to 1D array with `n` elements
324+
if x.size != n:
325+
raise ValueError(f"Cannot reshape array of size {x.size} to shape ({n},)")
326+
return x.reshape(n)
327+
else:
328+
# Reshape to 2D array with `n` rows and `k` columns
329+
if x.size != n * k:
330+
raise ValueError(f"Cannot reshape array of size {x.size} to shape ({n}, {k})")
331+
return x.reshape(n, k)

src/spotpython/utils/init.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def fun_control_init(
4949
eval=None,
5050
force_run=True,
5151
fun_evals=15,
52+
fun_mo2so=None,
5253
fun_repeats=1,
5354
horizon=None,
5455
hyperdict=None,
@@ -178,6 +179,9 @@ def fun_control_init(
178179
Default is False.
179180
fun_evals (int):
180181
The number of function evaluations.
182+
fun_mo2so (object):
183+
The multi-objective to single-objective transformation object. Default is None.
184+
If None, the first objective value is used in case of multi-objective optimization.
181185
fun_repeats (int):
182186
The number of function repeats during the optimization. this value does not affect
183187
the number of the repeats in the initial design (this value can be set in the
@@ -444,6 +448,7 @@ def fun_control_init(
444448
"eval": eval,
445449
"force_run": force_run,
446450
"fun_evals": fun_evals,
451+
"fun_mo2so": fun_mo2so,
447452
"fun_repeats": fun_repeats,
448453
"horizon": horizon,
449454
"hyperdict": hyperdict,

test/test_get_set_shape.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import pytest
2+
import numpy as np
3+
from spotpython.utils.convert import get_shape, set_shape
4+
5+
def test_get_shape():
6+
# Test 1D array
7+
x1 = np.array([1, 2, 3])
8+
assert get_shape(x1) == (3, None)
9+
10+
# Test 2D array
11+
x2 = np.array([[1, 2], [3, 4], [5, 6]])
12+
assert get_shape(x2) == (3, 2)
13+
14+
# Test empty 1D array
15+
x3 = np.array([])
16+
assert get_shape(x3) == (0, None)
17+
18+
# Test empty 2D array
19+
x4 = np.empty((0, 2))
20+
assert get_shape(x4) == (0, 2)
21+
22+
# Test invalid input (3D array)
23+
x5 = np.array([[[1, 2], [3, 4]]])
24+
with pytest.raises(ValueError, match="Input array must be 1D or 2D."):
25+
get_shape(x5)
26+
27+
28+
def test_set_shape():
29+
# Test reshaping to 1D
30+
x1 = np.array([1, 2, 3, 4])
31+
result = set_shape(x1, (4, None))
32+
assert result.shape == (4,)
33+
np.testing.assert_array_equal(result, np.array([1, 2, 3, 4]))
34+
35+
# Test reshaping to 2D
36+
x2 = np.array([1, 2, 3, 4])
37+
result = set_shape(x2, (2, 2))
38+
assert result.shape == (2, 2)
39+
np.testing.assert_array_equal(result, np.array([[1, 2], [3, 4]]))
40+
41+
# Test reshaping from 2D to 1D
42+
x3 = np.array([[1, 2], [3, 4]])
43+
result = set_shape(x3, (4, None))
44+
assert result.shape == (4,)
45+
np.testing.assert_array_equal(result, np.array([1, 2, 3, 4]))
46+
47+
# Test invalid reshape (size mismatch)
48+
x4 = np.array([1, 2, 3])
49+
with pytest.raises(ValueError, match="Cannot reshape array of size 3 to shape \\(4,\\)"):
50+
set_shape(x4, (4, None))
51+
52+
# Test invalid reshape (size mismatch for 2D)
53+
x5 = np.array([1, 2, 3, 4])
54+
with pytest.raises(ValueError, match="Cannot reshape array of size 4 to shape \\(3, 2\\)"):
55+
set_shape(x5, (3, 2))

test/test_mo2so.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import numpy as np
2+
from spotpython.spot.spot import Spot
3+
4+
class MockSpot(Spot):
5+
def __init__(self, fun_control):
6+
self.fun_control = fun_control
7+
8+
def test_mo2so():
9+
# Case 1: Multi-objective with a user-defined function
10+
fun_control = {"fun_mo2so": lambda y: np.sum(y, axis=0)} # Sum across objectives
11+
spot_instance = MockSpot(fun_control=fun_control)
12+
y_mo = np.array([[1, 2, 3], [4, 5, 6]]) # Shape (2, 3)
13+
result = spot_instance._mo2so(y_mo)
14+
expected = np.array([5, 7, 9]) # Sum of rows
15+
np.testing.assert_array_equal(result, expected)
16+
17+
# Case 2: Multi-objective without a user-defined function
18+
fun_control = {"fun_mo2so": None} # No function provided
19+
spot_instance = MockSpot(fun_control=fun_control)
20+
y_mo = np.array([[1, 2, 3], [4, 5, 6]]) # Shape (2, 3)
21+
result = spot_instance._mo2so(y_mo)
22+
expected = np.array([1, 2, 3]) # First row
23+
np.testing.assert_array_equal(result, expected)
24+
25+
# Case 3: Single-objective (no transformation needed)
26+
y_mo = np.array([[1, 2, 3]]) # Shape (1, 3)
27+
result = spot_instance._mo2so(y_mo)
28+
expected = np.array([[1, 2, 3]]) # No change
29+
np.testing.assert_array_equal(result, expected)
30+
31+
# Case 4: Single-objective with a single data point
32+
y_mo = np.array([[1]]) # Shape (1, 1)
33+
result = spot_instance._mo2so(y_mo)
34+
expected = np.array([[1]]) # No change
35+
np.testing.assert_array_equal(result, expected)
36+
37+
# Case 5: Empty input
38+
y_mo = np.array([[]]) # Shape (1, 0)
39+
result = spot_instance._mo2so(y_mo)
40+
expected = np.array([[]]) # No change
41+
np.testing.assert_array_equal(result, expected)

0 commit comments

Comments
 (0)