Coverage for moptipy / tests / objective.py: 68%

80 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 objective functions.""" 

2from math import inf, isfinite 

3from typing import Any, Callable, Final 

4 

5from numpy.random import Generator, default_rng 

6from pycommons.types import type_error 

7 

8from moptipy.api.objective import Objective, check_objective 

9from moptipy.api.space import Space 

10from moptipy.tests.component import validate_component 

11 

12 

13def validate_objective( 

14 objective: Objective, 

15 solution_space: Space | None = None, 

16 make_solution_space_element_valid: 

17 Callable[[Generator, Any], Any] | None = lambda _, x: x, 

18 is_deterministic: bool = True, 

19 lower_bound_threshold: int | float = -inf, 

20 upper_bound_threshold: int | float = inf, 

21 must_be_equal_to: Callable[[Any], int | float] | None = None) -> None: 

22 """ 

23 Check whether an object is a moptipy objective function. 

24 

25 :param objective: the objective function to test 

26 :param solution_space: the solution space 

27 :param make_solution_space_element_valid: a function that makes an element 

28 from the solution space valid 

29 :param bool is_deterministic: is the objective function deterministic? 

30 :param lower_bound_threshold: the threshold for the lower bound 

31 :param upper_bound_threshold: the threshold for the upper bound 

32 :param must_be_equal_to: an optional function that should return the 

33 exactly same values as the objective function 

34 :raises ValueError: if `objective` is not a valid 

35 :class:`~moptipy.api.objective.Objective` 

36 :raises TypeError: if values of the wrong types are encountered 

37 """ 

38 if not isinstance(objective, Objective): 

39 raise type_error(objective, "objective", Objective) 

40 check_objective(objective) 

41 validate_component(objective) 

42 

43 if not (hasattr(objective, "lower_bound") 

44 and callable(getattr(objective, "lower_bound"))): 

45 raise ValueError("objective must have method lower_bound.") 

46 lower: Final[int | float] = objective.lower_bound() 

47 if not (isinstance(lower, int | float)): 

48 raise type_error(lower, "lower_bound()", (int, float)) 

49 if (not isfinite(lower)) and (not (lower <= (-inf))): 

50 raise ValueError( 

51 f"lower bound must be finite or -inf, but is {lower}.") 

52 if lower < lower_bound_threshold: 

53 raise ValueError("lower bound must not be less than " 

54 f"{lower_bound_threshold}, but is {lower}.") 

55 

56 if not (hasattr(objective, "upper_bound") 

57 and callable(getattr(objective, "upper_bound"))): 

58 raise ValueError("objective must have method upper_bound.") 

59 upper: Final[int | float] = objective.upper_bound() 

60 if not (isinstance(upper, int | float)): 

61 raise type_error(upper, "upper_bound()", (int, float)) 

62 if (not isfinite(upper)) and (not (upper >= inf)): 

63 raise ValueError( 

64 f"upper bound must be finite or +inf, but is {upper}.") 

65 if upper > upper_bound_threshold: 

66 raise ValueError( 

67 f"upper bound must not be more than {upper_bound_threshold}, " 

68 f"but is {upper}.") 

69 

70 if lower >= upper: 

71 raise ValueError("Result of lower_bound() must be smaller than " 

72 f"upper_bound(), but got {lower} vs. {upper}.") 

73 

74 if not (hasattr(objective, "is_always_integer") 

75 and callable(getattr(objective, "is_always_integer"))): 

76 raise ValueError("objective must have method is_always_integer.") 

77 is_int: Final[bool] = objective.is_always_integer() 

78 if not isinstance(is_int, bool): 

79 raise type_error(is_int, "is_always_integer()", bool) 

80 if is_int: 

81 if isfinite(lower) and (not isinstance(lower, int)): 

82 raise TypeError( 

83 f"if is_always_integer()==True, then lower_bound() must " 

84 f"return int, but it returned {lower}.") 

85 if isfinite(upper) and (not isinstance(upper, int)): 

86 raise TypeError( 

87 f"if is_always_integer()==True, then upper_bound() must " 

88 f"return int, but it returned {upper}.") 

89 

90 count: int = 0 

91 if make_solution_space_element_valid is not None: 

92 count += 1 

93 if solution_space is not None: 

94 count += 1 

95 if count <= 0: 

96 return 

97 if count < 2: 

98 raise ValueError("either provide both of solution_space and " 

99 "make_solution_space_element_valid or none.") 

100 

101 x = solution_space.create() 

102 if x is None: 

103 raise ValueError("solution_space.create() produced None.") 

104 x = make_solution_space_element_valid(default_rng(), x) 

105 if x is None: 

106 raise ValueError("make_solution_space_element_valid() produced None.") 

107 solution_space.validate(x) 

108 

109 if not (hasattr(objective, "evaluate") 

110 and callable(getattr(objective, "evaluate"))): 

111 raise ValueError("objective must have method evaluate.") 

112 res = objective.evaluate(x) 

113 if not (isinstance(res, int | float)): 

114 raise type_error(res, f"evaluate(x) of {x}", (int, float)) 

115 

116 if (res < lower) or (res > upper): 

117 raise ValueError( 

118 f"evaluate(x) of {x} must return a value in [lower_bound() = " 

119 f"{lower}, upper_bound()={upper}], but returned {res}.") 

120 if is_int and (not isinstance(res, int)): 

121 raise TypeError( 

122 f"if is_always_integer()==True, then evaluate(x) must " 

123 f"return int, but it returned {res}.") 

124 

125 if must_be_equal_to is not None: 

126 exp = must_be_equal_to(x) 

127 if exp != res: 

128 raise ValueError(f"expected to get {exp}, but got {res}.") 

129 

130 res2 = objective.evaluate(x) 

131 if not (isinstance(res2, int | float)): 

132 raise type_error(res2, f"evaluate(x) of {x}", (int, float)) 

133 

134 if (res2 < lower) or (res2 > upper): 

135 raise ValueError(f"evaluate(x) of {x} must return a value in" 

136 "[lower_bound(), upper_bound()], but returned " 

137 f"{res2} vs. [{lower},{upper}].") 

138 if is_int and (not isinstance(res2, int)): 

139 raise TypeError( 

140 f"if is_always_integer()==True, then evaluate(x) must " 

141 f"return int, but it returned {res2}.") 

142 

143 if is_deterministic and (res != res2): 

144 raise ValueError(f"evaluating {x} twice yielded the two different " 

145 f"results {res} and {res2}!")