Coverage for moptipy / tests / op0.py: 80%
45 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
1"""Functions that can be used to test nullary search operators."""
2from math import isqrt
3from typing import Any, Callable, Final
5from numpy.random import Generator, default_rng
6from pycommons.types import check_int_range, type_error
8from moptipy.api.operators import Op0, check_op0
9from moptipy.api.space import Space
10from moptipy.tests.component import validate_component
13def validate_op0(op0: Op0,
14 search_space: Space | None = None,
15 make_search_space_element_valid:
16 Callable[[Generator, Any], Any] | None = lambda _, x: x,
17 number_of_samples: int = 100,
18 min_unique_samples: int | Callable[[int, Space], int]
19 = lambda samples, space:
20 max(1, min(samples // 2, isqrt(space.n_points())))) -> None:
21 """
22 Check whether an object is a valid moptipy nullary operator.
24 :param op0: the operator
25 :param search_space: the search space
26 :param make_search_space_element_valid: make a point in the search
27 space valid
28 :param number_of_samples: the number of times to invoke the operator
29 :param min_unique_samples: a lambda for computing the number
30 :raises ValueError: if `op0` is not a valid instance of
31 :class:`~moptipy.api.operators.Op0`
32 :raises TypeError: if invalid types are encountered
33 """
34 if not isinstance(op0, Op0):
35 raise type_error(op0, "op0", Op0)
36 if op0.__class__ == Op0:
37 raise ValueError("Cannot use abstract base Op0 directly.")
38 check_op0(op0)
39 validate_component(op0)
41 count: int = 0
42 if search_space is not None:
43 count += 1
44 if make_search_space_element_valid is not None:
45 count += 1
46 if count <= 0:
47 return
48 if count < 2:
49 raise ValueError(
50 "either provide both of search_space and "
51 "make_search_space_element_valid or none.")
52 check_int_range(number_of_samples, "number_of_samples", 1, 1_000_000)
53 random = default_rng()
54 x = search_space.create()
55 if x is None:
56 raise ValueError("Space must not return None.")
57 x = make_search_space_element_valid(random, x)
58 if x is None:
59 raise ValueError("Make valid failed.")
60 search_space.validate(x)
62 seen = set()
64 if not (hasattr(op0, "op0") and callable(getattr(op0, "op0"))):
65 raise ValueError("op0 must have method op0.")
66 for _ in range(number_of_samples):
67 op0.op0(random, x)
68 search_space.validate(x)
69 strstr = search_space.to_str(x)
70 if (not isinstance(strstr, str)) or (len(strstr) <= 0):
71 raise ValueError("to_str produces either no string or "
72 f"empty string, namely {strstr!r}.")
73 seen.add(strstr)
75 expected: Final[int] = check_int_range(min_unique_samples(
76 number_of_samples, search_space) if callable(
77 min_unique_samples) else min_unique_samples,
78 "expected", 1, number_of_samples)
79 if len(seen) < expected:
80 raise ValueError(
81 f"It is expected that at least {expected} different elements "
82 "will be created by nullary search operator from "
83 f"{number_of_samples} samples, but we only got {len(seen)} "
84 "different points.")