Source code for moptipy.tests.op2
"""Functions that can be used to test binary search operators."""
from math import isqrt
from typing import Any, Callable, Final
from numpy.random import Generator, default_rng
from pycommons.types import check_int_range, type_error
from moptipy.api.operators import Op2, check_op2
from moptipy.api.space import Space
from moptipy.tests.component import validate_component
[docs]
def validate_op2(op2: Op2,
search_space: Space | None = None,
make_search_space_element_valid:
Callable[[Generator, Any], Any] | None = lambda _, x: x,
number_of_samples: int = 100,
min_unique_samples: int | Callable[[int, Space], int]
= lambda samples, space:
max(1, min(samples // 2, isqrt(space.n_points())))) -> None:
"""
Check whether an object is valid a moptipy binary operator.
:param op2: the operator
:param search_space: the search space
:param make_search_space_element_valid: make a point in the search
space valid
:param number_of_samples: the number of times to invoke the operator
:param min_unique_samples: a lambda for computing the number
:raises ValueError: if `op2` is not a valid instance of
:class:`~moptipy.api.operators.Op2`
:raises TypeError: if incorrect types are encountered
"""
if not isinstance(op2, Op2):
raise type_error(op2, "op2", Op2)
if op2.__class__ == Op2:
raise ValueError("Cannot use abstract base Op2 directly.")
check_op2(op2)
validate_component(op2)
count: int = 0
if search_space is not None:
count += 1
if make_search_space_element_valid is not None:
count += 1
if count <= 0:
return
if count < 2:
raise ValueError(
"either provide both of search_space and "
"make_search_space_element_valid or none.")
check_int_range(number_of_samples, "number_of_samples", 1, 1_000_000)
random = default_rng()
x1 = search_space.create()
if x1 is None:
raise ValueError("Space must not return None.")
x1 = make_search_space_element_valid(random, x1)
if x1 is None:
raise ValueError("validator turned point to None?")
search_space.validate(x1)
seen = set()
strstr = search_space.to_str(x1)
if (not isinstance(strstr, str)) or (len(strstr) <= 0):
raise ValueError("to_str produces either no string or "
f"empty string, namely {strstr}.")
seen.add(strstr)
x2: Any = None
for _ in range(1000):
x2 = search_space.create()
if x2 is None:
raise ValueError("Space must not return None.")
x2 = make_search_space_element_valid(random, x2)
if x2 is None:
raise ValueError("validator turned point to None?")
if x1 is x2:
raise ValueError(
"Search space.create must not return same object instance.")
search_space.validate(x2)
strstr = search_space.to_str(x2)
if (not isinstance(strstr, str)) or (len(strstr) <= 0):
raise ValueError("to_str produces either no string or "
f"empty string, namely {strstr}.")
seen.add(strstr)
if len(seen) > 1:
break
if len(seen) <= 1:
raise ValueError("failed to create two different initial elements.")
x3 = search_space.create()
if x3 is None:
raise ValueError("Space must not return None.")
if (x1 is x3) or (x2 is x3):
raise ValueError(
"Search space.create must not return same object instance.")
if not (hasattr(op2, "op2") and callable(getattr(op2, "op2"))):
raise ValueError("op2 must have method op2.")
for _ in range(number_of_samples):
op2.op2(random, x3, x2, x1)
search_space.validate(x3)
strstr = search_space.to_str(x3)
if (not isinstance(strstr, str)) or (len(strstr) <= 0):
raise ValueError("to_str produces either no string or "
f"empty string, namely {strstr!r}.")
seen.add(strstr)
expected: Final[int] = check_int_range(min_unique_samples(
number_of_samples, search_space) if callable(
min_unique_samples) else min_unique_samples,
"expected", 1, number_of_samples)
if len(seen) < expected:
raise ValueError(
f"It is expected that at least {expected} different elements "
"will be created by unary search operator from "
f"{number_of_samples} samples, but we only "
f"got {len(seen)} different points.")