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
« 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
5from numpy.random import Generator, default_rng
6from pycommons.types import type_error
8from moptipy.api.objective import Objective, check_objective
9from moptipy.api.space import Space
10from moptipy.tests.component import validate_component
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.
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)
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}.")
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}.")
70 if lower >= upper:
71 raise ValueError("Result of lower_bound() must be smaller than "
72 f"upper_bound(), but got {lower} vs. {upper}.")
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}.")
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.")
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)
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))
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}.")
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}.")
130 res2 = objective.evaluate(x)
131 if not (isinstance(res2, int | float)):
132 raise type_error(res2, f"evaluate(x) of {x}", (int, float))
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}.")
143 if is_deterministic and (res != res2):
144 raise ValueError(f"evaluating {x} twice yielded the two different "
145 f"results {res} and {res2}!")