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

1"""Functions that can be used to test unary 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 Op1, check_op1 

9from moptipy.api.space import Space 

10from moptipy.tests.component import validate_component 

11 

12 

13def default_min_unique_samples(samples: int, space: Space) -> int: 

14 """ 

15 Compute the default number of minimum unique samples. 

16 

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()))) 

22 

23 

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. 

33 

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) 

50 

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) 

71 

72 seen = set() 

73 

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) 

79 

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.") 

86 

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) 

97 

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.")