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

1"""Functions that can be used to test nullary 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 Op0, check_op0 

9from moptipy.api.space import Space 

10from moptipy.tests.component import validate_component 

11 

12 

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. 

23 

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) 

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

61 

62 seen = set() 

63 

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) 

74 

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