Coverage for moptipy / tests / op2.py: 77%

73 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +0000

1"""Functions that can be used to test binary search operators.""" 

2from math import isqrt 

3from typing import Any, Callable, Final 

4 

5from numpy.random import Generator, default_rng 

6from pycommons.types import check_int_range, type_error 

7 

8from moptipy.api.operators import Op2, check_op2 

9from moptipy.api.space import Space 

10from moptipy.tests.component import validate_component 

11 

12 

13def validate_op2(op2: Op2, 

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 valid a moptipy binary operator. 

23 

24 :param op2: 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 `op2` is not a valid instance of 

31 :class:`~moptipy.api.operators.Op2` 

32 :raises TypeError: if incorrect types are encountered 

33 """ 

34 if not isinstance(op2, Op2): 

35 raise type_error(op2, "op2", Op2) 

36 if op2.__class__ == Op2: 

37 raise ValueError("Cannot use abstract base Op2 directly.") 

38 check_op2(op2) 

39 validate_component(op2) 

40 

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 x1 = search_space.create() 

55 if x1 is None: 

56 raise ValueError("Space must not return None.") 

57 x1 = make_search_space_element_valid(random, x1) 

58 if x1 is None: 

59 raise ValueError("validator turned point to None?") 

60 search_space.validate(x1) 

61 

62 seen = set() 

63 

64 strstr = search_space.to_str(x1) 

65 if (not isinstance(strstr, str)) or (len(strstr) <= 0): 

66 raise ValueError("to_str produces either no string or " 

67 f"empty string, namely {strstr}.") 

68 seen.add(strstr) 

69 x2: Any = None 

70 for _ in range(1000): 

71 x2 = search_space.create() 

72 if x2 is None: 

73 raise ValueError("Space must not return None.") 

74 x2 = make_search_space_element_valid(random, x2) 

75 if x2 is None: 

76 raise ValueError("validator turned point to None?") 

77 if x1 is x2: 

78 raise ValueError( 

79 "Search space.create must not return same object instance.") 

80 search_space.validate(x2) 

81 strstr = search_space.to_str(x2) 

82 if (not isinstance(strstr, str)) or (len(strstr) <= 0): 

83 raise ValueError("to_str produces either no string or " 

84 f"empty string, namely {strstr}.") 

85 seen.add(strstr) 

86 if len(seen) > 1: 

87 break 

88 if len(seen) <= 1: 

89 raise ValueError("failed to create two different initial elements.") 

90 

91 x3 = search_space.create() 

92 if x3 is None: 

93 raise ValueError("Space must not return None.") 

94 if (x1 is x3) or (x2 is x3): 

95 raise ValueError( 

96 "Search space.create must not return same object instance.") 

97 

98 if not (hasattr(op2, "op2") and callable(getattr(op2, "op2"))): 

99 raise ValueError("op2 must have method op2.") 

100 for _ in range(number_of_samples): 

101 op2.op2(random, x3, x2, x1) 

102 search_space.validate(x3) 

103 strstr = search_space.to_str(x3) 

104 if (not isinstance(strstr, str)) or (len(strstr) <= 0): 

105 raise ValueError("to_str produces either no string or " 

106 f"empty string, namely {strstr!r}.") 

107 seen.add(strstr) 

108 

109 expected: Final[int] = check_int_range(min_unique_samples( 

110 number_of_samples, search_space) if callable( 

111 min_unique_samples) else min_unique_samples, 

112 "expected", 1, number_of_samples) 

113 if len(seen) < expected: 

114 raise ValueError( 

115 f"It is expected that at least {expected} different elements " 

116 "will be created by unary search operator from " 

117 f"{number_of_samples} samples, but we only " 

118 f"got {len(seen)} different points.")