Skip to content

Commit 3321394

Browse files
0.29.21
sampling update
1 parent f146c87 commit 3321394

3 files changed

Lines changed: 128 additions & 1 deletion

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.29.20"
10+
version = "0.29.21"
1111
authors = [
1212
{ name="T. Bartz-Beielstein", email="tbb@bartzundbartz.de" }
1313
]

src/spotpython/utils/sampling.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,3 +951,44 @@ def mmphi_intensive(X: np.ndarray, q: Optional[float] = 2.0, p: Optional[float]
951951
return np.inf
952952

953953
return intensive_phiq
954+
955+
956+
def mmphi_intensive_update(X: np.ndarray, new_point: np.ndarray, J: np.ndarray, d: np.ndarray, q: float = 2.0, p: float = 2.0) -> tuple[float, np.ndarray, np.ndarray]:
957+
"""
958+
Updates the Morris-Mitchell intensive criterion for n+1 points by adding a new point to the design.
959+
960+
Args:
961+
X (np.ndarray): Existing sampling plan (shape: (n, d)).
962+
new_point (np.ndarray): New point to add (shape: (d,)).
963+
J (np.ndarray): Multiplicities of distances for the existing design.
964+
d (np.ndarray): Unique distances for the existing design.
965+
q (float): Exponent used in the computation of the metric. Defaults to 2.0.
966+
p (float): Distance norm to use (e.g., p=1 for Manhattan, p=2 for Euclidean). Defaults to 2.0.
967+
968+
Returns:
969+
tuple[float, np.ndarray, np.ndarray]: Updated intensive_phiq, updated_J, updated_d.
970+
"""
971+
n_points = X.shape[0]
972+
if n_points < 1:
973+
raise ValueError("The existing design must contain at least one point.")
974+
975+
# Compute distances between the new point and all existing points
976+
new_distances = np.array([np.linalg.norm(new_point - X[i], ord=p) for i in range(n_points)])
977+
978+
# Combine old distances and new distances into a single list
979+
all_distances = []
980+
for dist, count in zip(d, J):
981+
all_distances.extend([dist] * count)
982+
all_distances.extend(new_distances)
983+
984+
# Find unique distances and their counts
985+
updated_d, updated_J = np.unique(all_distances, return_counts=True)
986+
987+
# Calculate the number of unique pairs of points
988+
M = (n_points + 1) * n_points / 2
989+
990+
# Compute the updated intensive_phiq
991+
sum_term = np.sum(updated_J * (updated_d ** (-q)))
992+
intensive_phiq = (sum_term / M) ** (1.0 / q)
993+
994+
return intensive_phiq, updated_J, updated_d

test/test_sampling_mmphi_update.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 pytest
3+
from spotpython.utils.sampling import mmphi_intensive_update
4+
5+
def test_mmphi_intensive_update_basic():
6+
# 2 points in 2D
7+
X = np.array([[0.0, 0.0], [1.0, 0.0]])
8+
new_point = np.array([0.0, 1.0])
9+
# Initial distances and multiplicities (only one pair: distance 1.0)
10+
d = np.array([1.0])
11+
J = np.array([1])
12+
q = 2.0
13+
p = 2.0
14+
15+
intensive_phiq, updated_J, updated_d = mmphi_intensive_update(X, new_point, J, d, q, p)
16+
17+
# There are now 3 points, so 3 pairs: (0,1), (0,2), (1,2)
18+
# Distances: (0,1): 1.0, (0,2): 1.0, (1,2): sqrt(2)
19+
expected_d = np.array([1.0, np.sqrt(2)])
20+
expected_J = np.array([2, 1])
21+
assert np.allclose(np.sort(updated_d), np.sort(expected_d))
22+
assert np.sum(updated_J) == 3 # 3 pairs
23+
24+
# Check the value is finite and positive
25+
assert intensive_phiq > 0
26+
assert np.isfinite(intensive_phiq)
27+
28+
def test_mmphi_intensive_update_single_point_raises():
29+
X = np.empty((0, 2))
30+
new_point = np.array([0.0, 0.0])
31+
d = np.array([])
32+
J = np.array([])
33+
with pytest.raises(ValueError):
34+
mmphi_intensive_update(X, new_point, J, d)
35+
36+
def test_mmphi_intensive_update_duplicate_distances():
37+
"""
38+
Test mmphi_intensive_update with duplicate distances.
39+
"""
40+
X = np.array([[0.0], [1.0], [2.0]])
41+
new_point = np.array([3.0])
42+
d = np.array([1.0, 2.0])
43+
J = np.array([2, 1])
44+
q = 2.0
45+
p = 2.0
46+
47+
intensive_phiq, updated_J, updated_d = mmphi_intensive_update(X, new_point, J, d, q, p)
48+
49+
# Expected distances and counts
50+
expected_d = np.array([1.0, 2.0, 3.0])
51+
expected_J = np.array([3, 2, 1])
52+
53+
assert np.allclose(np.sort(updated_d), np.sort(expected_d))
54+
assert np.array_equal(updated_J, expected_J)
55+
assert np.sum(updated_J) == 6 # 4 points, 6 pairs
56+
57+
# Check the value is finite and positive
58+
assert intensive_phiq > 0
59+
assert np.isfinite(intensive_phiq)
60+
61+
def test_mmphi_intensive_update_nondefault_q_p():
62+
"""
63+
Test mmphi_intensive_update with non-default values of q and p.
64+
"""
65+
X = np.array([[0.0, 0.0], [1.0, 1.0]])
66+
new_point = np.array([0.0, 2.0])
67+
d = np.array([np.sqrt(2)]) # Existing distances should remain unchanged
68+
J = np.array([1]) # Existing counts should remain unchanged
69+
q = 1.0
70+
p = 1.0
71+
72+
intensive_phiq, updated_J, updated_d = mmphi_intensive_update(X, new_point, J, d, q, p)
73+
74+
# Expected distances and counts
75+
# Combine original distances with new distances calculated using Manhattan distance
76+
new_distances = np.array([np.sum(np.abs(X[0] - new_point)), np.sum(np.abs(X[1] - new_point))])
77+
all_distances = np.concatenate((d, new_distances))
78+
expected_d, expected_J = np.unique(all_distances, return_counts=True)
79+
80+
assert np.allclose(np.sort(updated_d), np.sort(expected_d))
81+
assert np.array_equal(updated_J, expected_J)
82+
assert np.sum(updated_J) == len(all_distances) # Total number of pairs
83+
84+
# Check the value is finite and positive
85+
assert intensive_phiq > 0
86+
assert np.isfinite(intensive_phiq)

0 commit comments

Comments
 (0)