Skip to content

Commit e434bc7

Browse files
0.27.10 doc
1 parent 7161d8c commit e434bc7

5 files changed

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

src/spotpython/design/utils.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import numpy as np
2+
import pandas as pd
3+
from typing import Union
4+
5+
6+
def get_boundaries(data: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
7+
"""
8+
Calculates the minimum and maximum values for each column in a NumPy array.
9+
10+
Args:
11+
data (np.ndarray): A NumPy array of shape (n, k), where n is the number of rows
12+
and k is the number of columns.
13+
14+
Returns:
15+
tuple[np.ndarray, np.ndarray]: A tuple containing two NumPy arrays:
16+
- The first array contains the minimum values for each column, with shape (k,).
17+
- The second array contains the maximum values for each column, with shape (k,).
18+
19+
Raises:
20+
ValueError: If the input array has shape (1, 0) (empty array).
21+
22+
Examples:
23+
>>> from spotpython.design.utils import get_boundaries
24+
>>> import numpy as np
25+
>>> data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
26+
>>> min_values, max_values = get_boundaries(data)
27+
>>> print("Minimum values:", min_values)
28+
Minimum values: [1 2 3]
29+
>>> print("Maximum values:", max_values)
30+
Maximum values: [7 8 9]
31+
"""
32+
if data.size == 0:
33+
raise ValueError("Input array cannot be empty.")
34+
min_values = np.min(data, axis=0)
35+
max_values = np.max(data, axis=0)
36+
return min_values, max_values
37+
38+
39+
def generate_search_grid(x_min: np.ndarray, x_max: np.ndarray, n_points: int = 5, col_names: list = None) -> Union[pd.DataFrame, np.ndarray]:
40+
"""
41+
Generates a search grid based on the minimum and maximum values of each feature.
42+
43+
Args:
44+
x_min (np.ndarray): A NumPy array containing the minimum values for each feature.
45+
x_max (np.ndarray): A NumPy array containing the maximum values for each feature.
46+
n_points (int, optional): The number of points to generate for each feature. Defaults to 5.
47+
col_names (list, optional): A list of column names for the DataFrame. If None, a NumPy array is returned. Defaults to None.
48+
49+
Returns:
50+
Union[pd.DataFrame, np.ndarray]: A Pandas DataFrame representing the search grid if col_names is provided,
51+
otherwise a NumPy array.
52+
53+
Raises:
54+
ValueError: If the length of x_min and x_max are different.
55+
56+
Examples:
57+
>>> from spotpython.design.utils import generate_search_grid
58+
>>> import numpy as np
59+
>>> x_min = np.array([0, 0, 0])
60+
>>> x_max = np.array([1, 1, 1])
61+
>>> search_grid = generate_search_grid(x_min, x_max, num_points=3)
62+
>>> print(search_grid)
63+
[[0. 0. 0. ]
64+
[0. 0. 0.5]
65+
[0. 0. 1. ]
66+
...
67+
[1. 1. 0.5]
68+
[1. 1. 1. ]]
69+
70+
>>> search_grid = generate_search_grid(x_min, x_max, num_points=3, col_names=['feature_0', 'feature_1', 'feature_2'])
71+
>>> print(search_grid)
72+
feature_0 feature_1 feature_2
73+
0 0.0 0.00 0.00
74+
1 0.0 0.00 0.50
75+
2 0.0 0.00 1.00
76+
3 0.0 0.50 0.00
77+
4 0.0 0.50 0.50
78+
.. ... ... ...
79+
22 1.0 1.00 0.50
80+
23 1.0 1.00 1.00
81+
82+
[27 rows x 3 columns]
83+
"""
84+
if len(x_min) != len(x_max):
85+
raise ValueError("x_min and x_max must have the same length.")
86+
87+
num_features = len(x_min)
88+
# Create linspace for each dimension
89+
ranges = [np.linspace(x_min[i], x_max[i], n_points) for i in range(num_features)]
90+
91+
# Use meshgrid to create all combinations
92+
# The maximum number of inputs for np.broadcast is 32
93+
if num_features > 30:
94+
raise ValueError("Too many features for meshgrid. Maximum 30 features are supported.")
95+
mesh = np.meshgrid(*ranges, indexing="ij")
96+
97+
# Reshape the meshgrid output to a list of points
98+
points = np.array([m.ravel() for m in mesh]).T
99+
100+
if col_names:
101+
# Create a Pandas DataFrame from the points
102+
if len(col_names) != num_features:
103+
raise ValueError("The number of column names must match the number of features.")
104+
search_grid = pd.DataFrame(points, columns=col_names)
105+
return search_grid
106+
else:
107+
return points
108+
109+
110+
def map_to_original_scale(X_search: Union[pd.DataFrame, np.ndarray], x_min: np.ndarray, x_max: np.ndarray) -> Union[pd.DataFrame, np.ndarray]:
111+
"""
112+
Maps the values in X_search from the range [0, 1] to the original scale defined by x_min and x_max.
113+
114+
Args:
115+
X_search (Union[pd.DataFrame, np.ndarray]): A Pandas DataFrame or NumPy array containing the search points in the range [0, 1].
116+
x_min (np.ndarray): A NumPy array containing the minimum values for each feature in the original scale.
117+
x_max (np.ndarray): A NumPy array containing the maximum values for each feature in the original scale.
118+
119+
Returns:
120+
Union[pd.DataFrame, np.ndarray]: A Pandas DataFrame or NumPy array with the values mapped to the original scale.
121+
122+
Examples:
123+
>>> from spotpython.design.utils import map_to_original_scale
124+
>>> import numpy as np
125+
>>> import pandas as pd
126+
>>> X_search = pd.DataFrame([[0.5, 0.5], [0.25, 0.75]], columns=['x', 'y'])
127+
>>> x_min = np.array([0, 0])
128+
>>> x_max = np.array([10, 20])
129+
>>> X_search_scaled = map_to_original_scale(X_search, x_min, x_max)
130+
>>> print(X_search_scaled)
131+
x y
132+
0 5.0 10.0
133+
1 2.5 15.0
134+
"""
135+
if not isinstance(X_search, (pd.DataFrame, np.ndarray)):
136+
raise TypeError("X_search must be a Pandas DataFrame or a NumPy array.")
137+
138+
if len(x_min) != X_search.shape[1]:
139+
raise IndexError(f"x_min and X_search must have the same number of columns. x_min has {len(x_min)} columns and X_search has {X_search.shape[1]} columns.")
140+
if len(x_max) != X_search.shape[1]:
141+
raise IndexError(f"x_max and X_search must have the same number of columns. x_max has {len(x_max)} columns and X_search has {X_search.shape[1]} columns.")
142+
143+
if isinstance(X_search, pd.DataFrame):
144+
X_search_scaled = X_search.copy() # Create a copy to avoid modifying the original DataFrame
145+
for i, col in enumerate(X_search.columns):
146+
X_search_scaled.loc[:, col] = X_search[col] * (x_max[i] - x_min[i]) + x_min[i]
147+
return X_search_scaled
148+
elif isinstance(X_search, np.ndarray):
149+
X_search_scaled = X_search.copy() # Create a copy to avoid modifying the original array
150+
for i in range(X_search.shape[1]):
151+
X_search_scaled[:, i] = X_search[:, i] * (x_max[i] - x_min[i]) + x_min[i]
152+
return X_search_scaled

src/spotpython/utils/desirability.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ class DesirabilityBase:
77
88
Provides a method to print class attributes and extend the range of values.
99
10-
Attributes:
11-
None
12-
1310
Methods:
1411
print_class_attributes(indent=0):
1512
Prints the attributes of the class object in a generic and recursive manner.
@@ -382,21 +379,22 @@ def __init__(self, *d_objs):
382379
Combines multiple desirability objects into an overall desirability function.
383380
384381
Args:
385-
*d_objs: Instances of desirability classes (e.g., DMax, DTarget, etc.).
382+
*d_objs (obj):
383+
Instances of desirability classes (e.g., DMax, DTarget, etc.).
386384
"""
387385
valid_classes = (DMax, DMin, DTarget, DArb, DBox, DCategorical)
388386
# print the instanaces of desirability classes
389-
print(f"d_objs: {d_objs}")
390-
for obj in d_objs:
391-
print(f"obj: {obj}")
392-
print(f"isinstance(obj, valid_classes): {isinstance(obj, valid_classes)}")
387+
# print(f"d_objs: {d_objs}")
388+
# for obj in d_objs:
389+
# print(f"obj: {obj}")
390+
# print(f"isinstance(obj, valid_classes): {isinstance(obj, valid_classes)}")
393391

394392
if not all(isinstance(obj, valid_classes) for obj in d_objs):
395393
raise ValueError("All objects must be instances of valid desirability classes.")
396394

397395
self.d_objs = d_objs # Store the desirability objects
398396

399-
def predict(self, newdata, all=False):
397+
def predict(self, newdata, all=False) -> float:
400398
"""
401399
Predicts the overall desirability based on the individual desirability objects.
402400
@@ -405,7 +403,8 @@ def predict(self, newdata, all=False):
405403
all (bool): Whether to return individual desirabilities along with the overall desirability.
406404
407405
Returns:
408-
float or tuple: The overall desirability score, or a tuple of individual and overall desirabilities if `all=True`.
406+
float or tuple:
407+
The overall desirability score, or a tuple of individual and overall desirabilities if `all=True`.
409408
"""
410409

411410
# # Compute individual desirabilities
@@ -501,7 +500,7 @@ def print_dOverall(self, digits=3, print_call=True):
501500
DesirabilityPrinter.print_dBox(d_obj, digits=digits, print_call=False)
502501

503502

504-
def conversion_pred(x):
503+
def conversion_pred(x) -> float:
505504
"""
506505
Predicts the percent conversion based on the input vector x.
507506
@@ -514,7 +513,7 @@ def conversion_pred(x):
514513
return 81.09 + 1.0284 * x[0] + 4.043 * x[1] + 6.2037 * x[2] - 1.8366 * x[0] ** 2 + 2.9382 * x[1] ** 2 - 5.1915 * x[2] ** 2 + 2.2150 * x[0] * x[1] + 11.375 * x[0] * x[2] - 3.875 * x[1] * x[2]
515514

516515

517-
def activity_pred(x):
516+
def activity_pred(x) -> float:
518517
"""
519518
Predicts the thermal activity based on the input vector x.
520519

test/test_design_utils.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import numpy as np
2+
import pandas as pd
3+
import pytest
4+
from spotpython.design.utils import get_boundaries, generate_search_grid
5+
6+
7+
def test_get_boundaries_with_positive_numbers():
8+
data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
9+
min_values, max_values = get_boundaries(data)
10+
assert np.array_equal(min_values, np.array([1, 2, 3]))
11+
assert np.array_equal(max_values, np.array([7, 8, 9]))
12+
13+
14+
def test_get_boundaries_with_negative_numbers():
15+
data = np.array([[-1, -2, -3], [-4, -5, -6], [-7, -8, -9]])
16+
min_values, max_values = get_boundaries(data)
17+
assert np.array_equal(min_values, np.array([-7, -8, -9]))
18+
assert np.array_equal(max_values, np.array([-1, -2, -3]))
19+
20+
21+
def test_get_boundaries_with_mixed_numbers():
22+
data = np.array([[1, -2, 3], [-4, 5, -6], [7, -8, 9]])
23+
min_values, max_values = get_boundaries(data)
24+
assert np.array_equal(min_values, np.array([-4, -8, -6]))
25+
assert np.array_equal(max_values, np.array([7, 5, 9]))
26+
27+
28+
def test_get_boundaries_with_single_row():
29+
data = np.array([[1, 2, 3]])
30+
min_values, max_values = get_boundaries(data)
31+
assert np.array_equal(min_values, np.array([1, 2, 3]))
32+
assert np.array_equal(max_values, np.array([1, 2, 3]))
33+
34+
35+
def test_get_boundaries_with_single_column():
36+
data = np.array([[1], [4], [7]])
37+
min_values, max_values = get_boundaries(data)
38+
assert np.array_equal(min_values, np.array([1]))
39+
assert np.array_equal(max_values, np.array([7]))
40+
41+
42+
def test_get_boundaries_with_empty_array():
43+
data = np.array([[]])
44+
with pytest.raises(ValueError):
45+
get_boundaries(data)
46+
47+
48+
def test_generate_search_grid_numpy():
49+
x_min = np.array([0, 0])
50+
x_max = np.array([1, 1])
51+
grid = generate_search_grid(x_min, x_max, n_points=3)
52+
assert isinstance(grid, np.ndarray)
53+
assert grid.shape == (9, 2)
54+
55+
56+
def test_generate_search_grid_pandas():
57+
x_min = np.array([0, 0])
58+
x_max = np.array([1, 1])
59+
col_names = ["x", "y"]
60+
grid = generate_search_grid(x_min, x_max, n_points=3, col_names=col_names)
61+
assert isinstance(grid, pd.DataFrame)
62+
assert grid.shape == (9, 2)
63+
assert list(grid.columns) == col_names
64+
65+
66+
def test_generate_search_grid_different_n_points():
67+
x_min = np.array([0, 0])
68+
x_max = np.array([1, 1])
69+
grid = generate_search_grid(x_min, x_max, n_points=5)
70+
assert grid.shape == (25, 2)
71+
72+
73+
def test_generate_search_grid_different_ranges():
74+
x_min = np.array([1, 2])
75+
x_max = np.array([4, 5])
76+
grid = generate_search_grid(x_min, x_max, n_points=3)
77+
assert np.allclose(grid[0], np.array([1.0, 2.0]))
78+
assert np.allclose(grid[-1], np.array([4.0, 5.0]))
79+
80+
81+
def test_generate_search_grid_col_names_mismatch():
82+
x_min = np.array([0, 0])
83+
x_max = np.array([1, 1])
84+
col_names = ["x"]
85+
with pytest.raises(ValueError):
86+
generate_search_grid(x_min, x_max, col_names=col_names)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import pytest
2+
import numpy as np
3+
import pandas as pd
4+
from spotpython.design.utils import map_to_original_scale
5+
6+
def test_map_to_original_scale_with_dataframe():
7+
X_search = pd.DataFrame([[0.5, 0.5], [0.25, 0.75]], columns=['x', 'y'])
8+
x_min = np.array([0, 0])
9+
x_max = np.array([10, 20])
10+
expected = pd.DataFrame([[5.0, 10.0], [2.5, 15.0]], columns=['x', 'y'])
11+
12+
result = map_to_original_scale(X_search, x_min, x_max)
13+
14+
pd.testing.assert_frame_equal(result, expected)
15+
16+
def test_map_to_original_scale_with_numpy_array():
17+
X_search = np.array([[0.5, 0.5], [0.25, 0.75]])
18+
x_min = np.array([0, 0])
19+
x_max = np.array([10, 20])
20+
expected = np.array([[5.0, 10.0], [2.5, 15.0]])
21+
22+
result = map_to_original_scale(X_search, x_min, x_max)
23+
24+
np.testing.assert_array_almost_equal(result, expected)
25+
26+
def test_map_to_original_scale_invalid_input_type():
27+
X_search = [[0.5, 0.5], [0.25, 0.75]] # Not a DataFrame or NumPy array
28+
x_min = np.array([0, 0])
29+
x_max = np.array([10, 20])
30+
31+
with pytest.raises(TypeError, match="X_search must be a Pandas DataFrame or a NumPy array."):
32+
map_to_original_scale(X_search, x_min, x_max)
33+
34+
def test_map_to_original_scale_mismatched_dimensions():
35+
X_search = np.array([[0.5, 0.5], [0.25, 0.75]])
36+
x_min = np.array([0])
37+
x_max = np.array([10])
38+
39+
with pytest.raises(IndexError, match="x_min and X_search must have the same number of columns."):
40+
map_to_original_scale(X_search, x_min, x_max)

0 commit comments

Comments
 (0)