Coverage for moptipy / tests / op1.py: 77%
56 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 unary 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 Op1, check_op1
9from moptipy.api.space import Space
10from moptipy.tests.component import validate_component
13def default_min_unique_samples(samples: int, space: Space) -> int:
14 """
15 Compute the default number of minimum unique samples.
17 :param samples: the number of samples
18 :param space: the space
19 :returns: the number of samples
20 """
21 return max(1, min(samples // 2, isqrt(space.n_points())))
24def validate_op1(op1: Op1,
25 search_space: Space | None = None,
26 make_search_space_element_valid:
27 Callable[[Generator, Any], Any] | None = lambda _, x: x,
28 number_of_samples: int = 100,
29 min_unique_samples: int | Callable[[int, Space], int]
30 = default_min_unique_samples) -> None:
31 """
32 Check whether an object is a valid moptipy unary operator.
34 :param op1: the operator
35 :param search_space: the search space
36 :param make_search_space_element_valid: make a point in the search
37 space valid
38 :param number_of_samples: the number of times to invoke the operator
39 :param min_unique_samples: a lambda for computing the number
40 :raises ValueError: if `op1` is not a valid instance of
41 :class:`~moptipy.api.operators.Op1`
42 :raises TypeError: if incorrect types are encountered
43 """
44 if not isinstance(op1, Op1):
45 raise type_error(op1, "op1", Op1)
46 if op1.__class__ == Op1:
47 raise ValueError("Cannot use abstract base Op1 directly.")
48 check_op1(op1)
49 validate_component(op1)
51 count: int = 0
52 if search_space is not None:
53 count += 1
54 if make_search_space_element_valid is not None:
55 count += 1
56 if count <= 0:
57 return
58 if count < 2:
59 raise ValueError(
60 "either provide both of search_space and "
61 "make_search_space_element_valid or none.")
62 check_int_range(number_of_samples, "number_of_samples", 1, 1_000_000)
63 random = default_rng()
64 x1 = search_space.create()
65 if x1 is None:
66 raise ValueError("Space must not return None.")
67 x1 = make_search_space_element_valid(random, x1)
68 if x1 is None:
69 raise ValueError("validator turned point to None?")
70 search_space.validate(x1)
72 seen = set()
74 strstr = search_space.to_str(x1)
75 if (not isinstance(strstr, str)) or (len(strstr) <= 0):
76 raise ValueError("to_str produces either no string or "
77 f"empty string, namely {strstr}.")
78 seen.add(strstr)
80 x2 = search_space.create()
81 if x2 is None:
82 raise ValueError("Space must not return None.")
83 if x1 is x2:
84 raise ValueError(
85 "Search space.create must not return same object instance.")
87 if not (hasattr(op1, "op1") and callable(getattr(op1, "op1"))):
88 raise ValueError("op1 must have method op1.")
89 for _ in range(number_of_samples):
90 op1.op1(random, x2, x1)
91 search_space.validate(x2)
92 strstr = search_space.to_str(x2)
93 if (not isinstance(strstr, str)) or (len(strstr) <= 0):
94 raise ValueError("to_str produces either no string or "
95 f"empty string, namely {strstr!r}.")
96 seen.add(strstr)
98 expected: Final[int] = check_int_range(min_unique_samples(
99 number_of_samples, search_space) if callable(
100 min_unique_samples) else min_unique_samples,
101 "expected", 1, number_of_samples)
102 if len(seen) < expected:
103 raise ValueError(
104 f"It is expected that at least {expected} different elements "
105 "will be created by unary search operator from "
106 f"{number_of_samples} samples, but we only "
107 f"got {len(seen)} different points.")