Skip to content

Commit b995339

Browse files
committed
More Python tests for wrong ref in hypervolume and negative input in epsilon_mult().
* python/src/moocore/_utils.py (array_1d_of_length_n): Add 'name' argument. * python/src/moocore/_moocore.py: Use it everywhere. Delete unreachable condition. * python/tests/test_moocore.py (test_wrong_ref): Add more tests. (check_roll_column, test_epsilon_mult_negative_input): New.
1 parent ec9cd00 commit b995339

3 files changed

Lines changed: 71 additions & 43 deletions

File tree

python/src/moocore/_moocore.py

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def read_datasets(filename: str | os.PathLike | StringIO) -> np.ndarray:
154154

155155
def _parse_maximise(maximise: bool | Sequence[bool], nobj: int) -> np.ndarray:
156156
"""Convert maximise array or single bool to ndarray format."""
157-
return array_1d_of_length_n(maximise, nobj).astype(bool)
157+
return array_1d_of_length_n(maximise, nobj, name="maximise").astype(bool)
158158

159159

160160
def _parse_maximise_to_bool_array(
@@ -532,14 +532,10 @@ def hypervolume(
532532
# an int array.
533533
data, data_copied = asarray_maybe_copy(data)
534534
nobj = data.shape[1]
535-
# Make sure it is a 1D array of length nobj.
536-
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj)
537-
if nobj != ref.shape[0]:
538-
raise ValueError(
539-
f"data and ref need to have the same number of objectives ({nobj} != {ref.shape[0]})"
540-
)
541535
if nobj == 0:
542536
raise ValueError("input data must have at least 1 column")
537+
# Make sure it is a 1D array of length nobj.
538+
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj, name="ref")
543539

544540
maximise = _parse_maximise(maximise, nobj)
545541
# FIXME: Do this in C.
@@ -826,12 +822,7 @@ def hv_contributions(
826822
# an int array.
827823
x, x_copied = asarray_maybe_copy(x)
828824
nobj = x.shape[1]
829-
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj)
830-
if nobj != ref.shape[0]:
831-
raise ValueError(
832-
f"data and ref need to have the same number of objectives ({nobj} != {ref.shape[0]})"
833-
)
834-
825+
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj, name="ref")
835826
maximise = _parse_maximise(maximise, nobj)
836827
# FIXME: Do this in C.
837828
if maximise.any():
@@ -1006,11 +997,7 @@ def hv_approx(
1006997
# an int array.
1007998
data = np.asarray(data, dtype=float)
1008999
nobj = data.shape[1]
1009-
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj)
1010-
if nobj != ref.shape[0]:
1011-
raise ValueError(
1012-
f"data and ref need to have the same number of objectives ({nobj} != {ref.shape[0]})"
1013-
)
1000+
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj, name="ref")
10141001

10151002
if not is_integer_value(nsamples):
10161003
raise ValueError(f"nsamples must be an integer value: {nsamples}")
@@ -1766,8 +1753,12 @@ def normalise(
17661753
to_range = np.asarray(to_range, dtype=float)
17671754
if to_range.shape[0] != 2:
17681755
raise ValueError("'to_range' must have length 2")
1769-
lower = array_1d_of_length_n(np.asarray(lower, dtype=float), nobj)
1770-
upper = array_1d_of_length_n(np.asarray(upper, dtype=float), nobj)
1756+
lower = array_1d_of_length_n(
1757+
np.asarray(lower, dtype=float), nobj, name="lower"
1758+
)
1759+
upper = array_1d_of_length_n(
1760+
np.asarray(upper, dtype=float), nobj, name="upper"
1761+
)
17711762
if np.any(np.isnan(lower)):
17721763
lower = np.where(np.isnan(lower), data.min(axis=0), lower)
17731764
if np.any(np.isnan(upper)):
@@ -2295,7 +2286,7 @@ def whv_rect(
22952286
# else:
22962287
# pos = np.flatnonzero(maximise) + [0,2]
22972288
# rectangles[:, pos] = -rectangles[:, pos]
2298-
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj)
2289+
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj, name="ref")
22992290
ref = ffi.from_buffer("double []", ref)
23002291
x, npoints, _ = np2d_to_double_array(x)
23012292
rectangles, rectangles_nrow, _ = np2d_to_double_array(rectangles)
@@ -2401,14 +2392,16 @@ def total_whv_rect(
24012392
if scalefactor <= 0 or scalefactor > 1:
24022393
raise ValueError("'scalefactor' must be within (0,1]")
24032394

2404-
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj)
2395+
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj, name="ref")
24052396
whv = whv_rect(x, rectangles, ref=ref, maximise=maximise)
24062397
hv = hypervolume(x, ref=ref) # FIXME: maximise = maximise)
24072398
if ideal is None:
24082399
# FIXME: Should we include the range of the rectangles here?
24092400
ideal = get_ideal(x, maximise=maximise)
24102401
else:
2411-
ideal = array_1d_of_length_n(np.asarray(ideal, dtype=float), nobj)
2402+
ideal = array_1d_of_length_n(
2403+
np.asarray(ideal, dtype=float), nobj, name="ideal"
2404+
)
24122405

24132406
beta = scalefactor * abs((ref - ideal).prod())
24142407
return float(hv + beta * whv)
@@ -2661,8 +2654,10 @@ def whv_hype(
26612654
if nobj != 2:
26622655
raise NotImplementedError("Only 2D datasets are currently supported")
26632656

2664-
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj)
2665-
ideal = array_1d_of_length_n(np.asarray(ideal, dtype=float), nobj)
2657+
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj, name="ref")
2658+
ideal = array_1d_of_length_n(
2659+
np.asarray(ideal, dtype=float), nobj, name="ideal"
2660+
)
26662661
maximise = _parse_maximise(maximise, nobj)
26672662
# FIXME: Do this in C.
26682663
if maximise.any():
@@ -2688,7 +2683,7 @@ def whv_hype(
26882683
mu = ffi.cast("double", mu)
26892684
hv = lib.whv_hype_expo(data_p, npoints, ideal, ref, nsamples, seed, mu)
26902685
elif dist == "point":
2691-
mu = array_1d_of_length_n(np.asarray(mu, dtype=float), nobj)
2686+
mu = array_1d_of_length_n(np.asarray(mu, dtype=float), nobj, name="mu")
26922687
mu, _ = np1d_to_double_array(mu)
26932688
hv = lib.whv_hype_gaus(data_p, npoints, ideal, ref, nsamples, seed, mu)
26942689
else:
@@ -2880,14 +2875,10 @@ def r2_exact(
28802875
# an int array.
28812876
data, data_copied = asarray_maybe_copy(data)
28822877
nobj = data.shape[1]
2883-
# Make sure it is a 1D array of length nobj.
2884-
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj)
2885-
if nobj != ref.shape[0]:
2886-
raise ValueError(
2887-
f"data and ref need to have the same number of objectives ({nobj} != {ref.shape[0]})"
2888-
)
28892878
if nobj != 2:
28902879
raise NotImplementedError("Only 2D datasets are currently supported")
2880+
# Make sure it is a 1D array of length nobj.
2881+
ref = array_1d_of_length_n(np.asarray(ref, dtype=float), nobj, name="ref")
28912882

28922883
maximise = _parse_maximise(maximise, nobj)
28932884
# FIXME: Do this in C.

python/src/moocore/_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,14 @@ def np1d_to_int_array(
6161
return np1d_to_c_array(x, ctype_data="int", ctype_size=ctype_size)
6262

6363

64-
def array_1d_of_length_n(x: ArrayLike, n: int) -> np.ndarray:
64+
def array_1d_of_length_n(x: ArrayLike, n: int, name: str = "x") -> np.ndarray:
6565
x = np.ravel(x)
6666
if len(x) == 1:
6767
return np.full((n), x[0])
6868
if x.shape[0] == n:
6969
return x
7070
raise ValueError(
71-
f"1D array must have length {n} but it has length {x.shape[0]}"
71+
f"{name!r} must have length {n}, but it has length {x.shape[0]}"
7272
)
7373

7474

python/tests/test_moocore.py

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,22 +100,35 @@ def test_hv_output(self, test_datapath, immutable_call):
100100

101101
ref = np.array([2, -2, 2], dtype=float)
102102
x = [[1, 0, 1], [0, -1, 0]]
103+
maximise = [False, True, False]
103104
assert (
104-
immutable_call(
105-
moocore.hypervolume, x, ref, maximise=[False, True, False]
106-
)
105+
immutable_call(moocore.hypervolume, x, ref, maximise=maximise)
107106
== 5.0
108107
)
108+
hv = moocore.Hypervolume(ref=ref, maximise=maximise)
109+
assert immutable_call(hv, x) == 5.0
109110

110111
ref = [-2, -2, -2]
111112
x = [[-1, 0, -1], [0, -1, 0]]
112113
assert immutable_call(moocore.hypervolume, x, ref, maximise=True) == 5.0
113114

114-
def test_hv_wrong_ref(self, test_datapath):
115-
"""Check that the moocore.hypervolume() fails correctly after a ref with the wrong dimensions is input."""
116-
X = self.input1
117-
with pytest.raises(ValueError):
118-
moocore.hypervolume(X[X[:, 2] == 1, :2], ref=np.array([10, 10, 10]))
115+
116+
def test_wrong_ref():
117+
"""Check that the moocore.hypervolume() fails correctly after a ref with the wrong dimensions is input."""
118+
x = [[1, 1, 1]]
119+
ref = [10, 10]
120+
hv = moocore.Hypervolume(ref=ref)
121+
with pytest.raises(ValueError, match="same number of"):
122+
hv(x)
123+
124+
with pytest.raises(ValueError, match="must have length 3"):
125+
moocore.hypervolume(x, ref=ref)
126+
127+
with pytest.raises(ValueError, match="must have length 3"):
128+
moocore.hv_contributions(x, ref=ref)
129+
130+
with pytest.raises(ValueError, match="must have length 3"):
131+
moocore.hv_approx(x, ref=ref)
119132

120133

121134
@pytest.mark.parametrize("dim", range(5, 9))
@@ -229,6 +242,7 @@ def test_igd():
229242

230243
@pytest.mark.parametrize("dim", range(0, 3))
231244
def test_is_nondominated_keep_weakly(dim):
245+
232246
def check_keep_weakly(x, true_ndom, true_wndom):
233247
true_ndom = np.array(true_ndom)
234248
true_wndom = np.array(true_wndom)
@@ -242,6 +256,17 @@ def check_keep_weakly(x, true_ndom, true_wndom):
242256
test_weak_filter = moocore.filter_dominated(x, keep_weakly=True)
243257
assert_array_equal(test_weak_filter, x[true_wndom, :])
244258

259+
def check_roll_column(x, dim):
260+
true_ndom = moocore.is_nondominated(x)
261+
true_wndom = moocore.is_nondominated(x, keep_weakly=True)
262+
a = np.append(np.zeros((len(x), dim)), x, axis=1)
263+
for i in range(a.shape[1]):
264+
x = np.roll(a, i, axis=1)
265+
assert_array_equal(true_ndom, moocore.is_nondominated(x))
266+
assert_array_equal(
267+
true_wndom, moocore.is_nondominated(x, keep_weakly=True)
268+
)
269+
245270
x = np.array(
246271
[[2, 0], [1, 1], [3, 0], [2, 0], [1, 2], [3, 0], [0, 2], [1, 1], [1, 1]]
247272
)
@@ -250,6 +275,7 @@ def check_keep_weakly(x, true_ndom, true_wndom):
250275
true_ndom=[True, True, False, False, False, False, True, False, False],
251276
true_wndom=[True, True, False, True, False, False, True, True, True],
252277
)
278+
check_roll_column(np.vstack((x, x)), dim)
253279

254280
x = np.array(
255281
[[1, 0, 1], [1, 1, 1], [0, 1, 1], [1, 0, 1], [1, 1, 0], [1, 1, 1]]
@@ -259,6 +285,8 @@ def check_keep_weakly(x, true_ndom, true_wndom):
259285
true_ndom=[True, False, True, False, True, False],
260286
true_wndom=[True, False, True, True, True, False],
261287
)
288+
# KUNG_SMALL_THRESHOLD is 16, so use 18 points.
289+
check_roll_column(np.vstack((x, x, x)), dim)
262290

263291

264292
def test_is_nondominated(test_datapath):
@@ -299,7 +327,7 @@ def test_is_nondominated(test_datapath):
299327
)
300328

301329

302-
def test_epsilon(immutable_call):
330+
def test_epsilon():
303331
"""Same as in R package."""
304332
ref = np.array([10, 1, 6, 1, 2, 2, 1, 6, 1, 10]).reshape((-1, 2))
305333
A = np.array([4, 2, 3, 3, 2, 4]).reshape((-1, 2))
@@ -313,6 +341,15 @@ def test_epsilon(immutable_call):
313341
assert_expected(2.5, moocore.epsilon_mult, A, ref, maximise=[False, True])
314342

315343

344+
def test_epsilon_mult_negative_input():
345+
x = np.array([[-1, 2]])
346+
y = np.array([[1, 2]])
347+
with pytest.raises(ValueError, match="larger than 0"):
348+
moocore.epsilon_mult(x, ref=y)
349+
with pytest.raises(ValueError, match="larger than 0"):
350+
moocore.epsilon_mult(y, ref=x)
351+
352+
316353
@pytest.mark.parametrize("dim", range(3, 6))
317354
def test_epsilon_dim(dim):
318355
seed = np.random.default_rng().integers(2**32 - 2)

0 commit comments

Comments
 (0)