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
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
1"""Some internal helper functions."""
3from typing import Final
5import numba # type: ignore
6import numpy as np
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.
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.
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}.")
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.
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
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)