Coverage for moptipy / tests / mo_problem.py: 69%

110 statements  

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

1"""Functions for testing multi-objective optimization problems.""" 

2from math import inf, isfinite 

3from typing import Any, Callable, Final 

4 

5import numpy as np 

6from numpy.random import Generator, default_rng 

7from pycommons.types import check_int_range, type_error 

8 

9from moptipy.api.mo_problem import MOProblem, check_mo_problem 

10from moptipy.api.space import Space 

11from moptipy.tests.objective import validate_objective 

12from moptipy.utils.nputils import is_np_float, is_np_int 

13 

14 

15def validate_mo_problem( 

16 mo_problem: MOProblem, 

17 solution_space: Space | None = None, 

18 make_solution_space_element_valid: 

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

20 is_deterministic: bool = True, 

21 lower_bound_threshold: int | float = -inf, 

22 upper_bound_threshold: int | float = inf, 

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

24 """ 

25 Check whether an object is a moptipy multi-objective optimization problem. 

26 

27 :param mo_problem: the multi-objective optimization problem to test 

28 :param solution_space: the solution space 

29 :param make_solution_space_element_valid: a function that makes an element 

30 from the solution space valid 

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

32 :param lower_bound_threshold: the threshold for the lower bound 

33 :param upper_bound_threshold: the threshold for the upper bound 

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

35 exactly same values as the objective function 

36 :raises ValueError: if `mo_problem` is not a valid 

37 :class:`~moptipy.api.mo_problem.MOProblem` 

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

39 """ 

40 if not isinstance(mo_problem, MOProblem): 

41 raise type_error(mo_problem, "mo_problem", MOProblem) 

42 check_mo_problem(mo_problem) 

43 validate_objective(mo_problem, solution_space, 

44 make_solution_space_element_valid, is_deterministic, 

45 lower_bound_threshold, upper_bound_threshold, 

46 must_be_equal_to) 

47 

48 dim: Final[int] = check_int_range(mo_problem.f_dimension(), 

49 "f_dimension()", 1, 100_000) 

50 all_int: Final[bool] = mo_problem.is_always_integer() 

51 fses: Final[tuple[np.ndarray, np.ndarray]] = \ 

52 mo_problem.f_create(), mo_problem.f_create() 

53 

54 exp_dtype: Final[np.dtype] = mo_problem.f_dtype() 

55 if not isinstance(exp_dtype, np.dtype): 

56 raise type_error(exp_dtype, "exp_dtype", np.dtype) 

57 

58 if is_np_float(exp_dtype): 

59 if all_int: 

60 raise ValueError(f"if f_dtype()=={exp_dtype}, " 

61 f"is_always_integer() must not be {all_int}") 

62 elif not is_np_int(exp_dtype): 

63 raise ValueError(f"f_dtype() cannot be {exp_dtype}") 

64 

65 if fses[0] is fses[1]: 

66 raise ValueError("f_create returns same array!") 

67 

68 shape: Final[tuple[int]] = (dim, ) 

69 for fs in fses: 

70 if not isinstance(fs, np.ndarray): 

71 raise type_error(fs, "f_create()", np.ndarray) 

72 if len(fs) != dim: 

73 raise ValueError( 

74 f"len(f_create()) == {len(fs)} but f_dimension()=={dim}.") 

75 if fs.shape != shape: 

76 raise ValueError( 

77 f"f_create().shape={fs.shape}, but must be {shape}.") 

78 if fs.dtype != exp_dtype: 

79 raise ValueError( 

80 f"f_dtype()={exp_dtype} but f_create().dtype={fs.dtype}.") 

81 if not isinstance(all_int, bool): 

82 raise type_error(all_int, "is_always_integer()", bool) 

83 is_int: bool = is_np_int(fs.dtype) 

84 if not isinstance(is_int, bool): 

85 raise type_error(is_np_int, "is_np_int(dtype)", bool) 

86 is_float: bool = is_np_float(fs.dtype) 

87 if not isinstance(is_float, bool): 

88 raise type_error(is_float, "is_np_float(dtype)", bool) 

89 if not (is_int ^ is_float): 

90 raise ValueError(f"dtype ({fs.dtype}) of f_create() must be " 

91 f"either int ({is_int}) or float ({is_float}).") 

92 if all_int and not is_int: 

93 raise ValueError(f"if is_always_integer()=={all_int}, then the " 

94 f"dtype ({fs.dtype}) of f_create() must be an " 

95 f"integer type, but is not ({is_int}).") 

96 fs1: np.ndarray 

97 fs2: np.ndarray 

98 fs1, fs2 = fses 

99 if fs1.dtype is not fs2.dtype: 

100 raise ValueError("encountered two different dtypes when invoking " 

101 f"f_create() twice: {fs1.dtype}, {fs2.dtype}") 

102 

103 lower: Final[int | float] = mo_problem.lower_bound() 

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

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

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

107 raise ValueError( 

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

109 if lower < lower_bound_threshold: 

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

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

112 

113 upper: Final[int | float] = mo_problem.upper_bound() 

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

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

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

117 raise ValueError( 

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

119 if upper > upper_bound_threshold: 

120 raise ValueError( 

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

122 f"but is {lower}.") 

123 

124 if lower >= upper: 

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

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

127 

128 count: int = 0 

129 if make_solution_space_element_valid is not None: 

130 count += 1 

131 if solution_space is not None: 

132 count += 1 

133 if count <= 0: 

134 return 

135 if count < 2: 

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

137 "make_solution_space_element_valid or none.") 

138 

139 x = solution_space.create() 

140 if x is None: 

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

142 random: Final[Generator] = default_rng() 

143 x = make_solution_space_element_valid(random, x) 

144 if x is None: 

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

146 solution_space.validate(x) 

147 

148 reses: Final[list[int | float]] = [ 

149 mo_problem.f_evaluate(x, fs1), mo_problem.f_evaluate(x, fs2)] 

150 if len(reses) != 2: 

151 raise ValueError(f"Huh? {len(reses)} != 2 for {reses}??") 

152 

153 for fs in fses: 

154 for v in fs: 

155 if not isfinite(v): 

156 raise ValueError(f"encountered non-finite value {v} in " 

157 f"objective vector {fs} of {x}.") 

158 mo_problem.f_validate(fs) 

159 

160 fdr = mo_problem.f_dominates(fses[0], fses[1]) 

161 if fdr != 2: 

162 raise ValueError(f"f_dominates(x, x) must be 2, but is {fdr}") 

163 

164 for res in reses: 

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

166 raise type_error(res, "f_evaluate(x)", (int, float)) 

167 if not isfinite(res): 

168 raise ValueError( 

169 f"result of f_evaluate() must be finite, but is {res}.") 

170 if res < lower: 

171 raise ValueError(f"f_evaluate()={res} < lower_bound()={lower}") 

172 if res > upper: 

173 raise ValueError(f"f_evaluate()={res} > upper_bound()={upper}") 

174 if is_deterministic: 

175 if not np.array_equal(fs1, fs2): 

176 raise ValueError("deterministic objective returns vectors " 

177 f"{fses} when evaluating {x}.") 

178 if reses[0] != reses[1]: 

179 raise ValueError("deterministic objective returns scalar " 

180 f"{reses} when evaluating {x}.")