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
« 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
5import numpy as np
6from numpy.random import Generator, default_rng
7from pycommons.types import check_int_range, type_error
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
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.
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)
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()
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)
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}")
65 if fses[0] is fses[1]:
66 raise ValueError("f_create returns same array!")
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}")
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}.")
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}.")
124 if lower >= upper:
125 raise ValueError("Result of lower_bound() must be smaller than "
126 f"upper_bound(), but got {lower} vs. {upper}.")
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.")
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)
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}??")
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)
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}")
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}.")