Coverage for moptipy / evaluation / _utils.py: 62%

16 statements  

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

1"""Some internal helper functions.""" 

2 

3from typing import Final 

4 

5import numba # type: ignore 

6import numpy as np 

7 

8 

9def _check_max_time_millis(max_time_millis: int | float, 

10 total_fes: int | float, 

11 total_time_millis: int | float) -> None: 

12 """ 

13 Check whether a max-time-millis value is permissible. 

14 

15 If we set a time limit for a run, then the 

16 :meth:`~moptipy.api.process.Process.should_terminate` will become `True` 

17 approximately after the time limit has expired. However, this also 

18 could be later (or maybe even earlier) due to the workings of the 

19 underlying operating system. And even if 

20 :meth:`~moptipy.api.process.Process.should_terminate` if `True`, it is not 

21 clear whether the optimization algorithm can query it right away. 

22 Instead, it may be blocked in a long-running objective function 

23 evaluation or some other computation. Hence, it may actually stop 

24 later. So we cannot simply require that 

25 `total_time_millis <= max_time_millis`, as this is not practically 

26 enforceable. Instead, we will heuristically determine a feasible 

27 maximum limit for how much longer an algorithm might run. 

28 

29 :param max_time_millis: the max time millis threshold 

30 :param total_fes: the total FEs performed 

31 :param total_time_millis: the measured total time millis 

32 """ 

33 if total_fes == 1: 

34 return 

35 div: Final[float] = (total_fes - 2) if total_fes > 2 else 1 

36 permitted_limit: Final[float] = \ 

37 60_000 + ((1 + (1.1 * (max_time_millis / div))) * total_fes) 

38 if total_time_millis > permitted_limit: 

39 raise ValueError( 

40 f"If max_time_millis is {max_time_millis} and " 

41 f"total_fes is {total_fes}, then total_time_millis must " 

42 f"not be more than {permitted_limit}, but is " 

43 f"{total_time_millis}.") 

44 

45 

46@numba.njit(nogil=True) 

47def _get_goal_reach_index(f: np.ndarray, goal_f: int | float) -> int: # noqa 

48 """ 

49 Compute the offset from the end of `f` when `goal_f` was reached. 

50 

51 :param f: the raw data array, which must be sorted in 

52 descending order 

53 :param goal_f: the goal f value 

54 :return: the index, or `-1` if `goal_f` was not reached 

55 

56 >>> ft = np.array([10, 9, 8, 5, 3, 2, 1]) 

57 >>> _get_goal_reach_index(ft, 11) 

58 0 

59 >>> int(ft[_get_goal_reach_index(ft, 11)]) 

60 10 

61 >>> _get_goal_reach_index(ft, 10) 

62 0 

63 >>> _get_goal_reach_index(ft, 9) 

64 1 

65 >>> int(ft[_get_goal_reach_index(ft, 6)]) 

66 5 

67 >>> _get_goal_reach_index(ft, 1) 

68 6 

69 >>> _get_goal_reach_index(ft, 0.9) 

70 -1 

71 >>> ft = np.array([10, 9, 8, 5, 5, 3, 2, 1]) 

72 >>> _get_goal_reach_index(ft, 8) 

73 2 

74 >>> _get_goal_reach_index(ft, 7) 

75 3 

76 >>> _get_goal_reach_index(ft, 6) 

77 3 

78 >>> _get_goal_reach_index(ft, 5) 

79 3 

80 >>> _get_goal_reach_index(ft, 4) 

81 5 

82 >>> ft = np.array([10, 9, 9, 8, 5, 5, 3, 2, 1]) 

83 >>> _get_goal_reach_index(ft, 9) 

84 1 

85 >>> ft = np.array([10, 9, 9, 9, 8, 5, 5, 3, 2, 1]) 

86 >>> _get_goal_reach_index(ft, 9) 

87 1 

88 >>> ft = np.array([10, 9, 9, 9, 9, 8, 5, 5, 3, 2, 1]) 

89 >>> _get_goal_reach_index(ft, 9) 

90 1 

91 >>> ft = np.array([10]) 

92 >>> _get_goal_reach_index(ft, 10) 

93 0 

94 >>> ft = np.array([10, 10]) 

95 >>> _get_goal_reach_index(ft, 10) 

96 0 

97 >>> ft = np.array([10, 10, 10]) 

98 >>> _get_goal_reach_index(ft, 10) 

99 0 

100 >>> ft = np.array([10, 10, 10, 10]) 

101 >>> _get_goal_reach_index(ft, 10) 

102 0 

103 >>> ft = np.array([10, 9]) 

104 >>> _get_goal_reach_index(ft, 9) 

105 1 

106 >>> ft = np.array([10, 9, 9]) 

107 >>> _get_goal_reach_index(ft, 9) 

108 1 

109 >>> ft = np.array([10, 9, 9, 9]) 

110 >>> _get_goal_reach_index(ft, 9) 

111 1 

112 >>> ft = np.array([10, 9, 9, 9, 9]) 

113 >>> _get_goal_reach_index(ft, 9) 

114 1 

115 >>> ft = np.array([10, 9, 9, 9, 9, 8]) 

116 >>> _get_goal_reach_index(ft, 9) 

117 1 

118 >>> ft = np.array([10, 9, 9, 9, 8, 8]) 

119 >>> _get_goal_reach_index(ft, 9) 

120 1 

121 >>> ft = np.array([10, 9, 9, 8, 8, 8]) 

122 >>> _get_goal_reach_index(ft, 9) 

123 1 

124 >>> ft = np.array([10, 9, 8, 8, 8, 8]) 

125 >>> _get_goal_reach_index(ft, 9) 

126 1 

127 >>> ft = np.array([10, 8, 8, 8, 8, 8]) 

128 >>> _get_goal_reach_index(ft, 9) 

129 1 

130 """ 

131 res: Final = np.searchsorted(f[::-1], goal_f, side="right") 

132 if res <= 0: 

133 return -1 

134 return int(f.size - res)