Coverage for moptipy / mock / objective.py: 83%
121 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"""A mock-up of an objective function."""
3from math import inf, isfinite, nextafter
4from typing import Any, Final, Iterable, cast
6import numpy as np
7from numpy.random import Generator, default_rng
8from pycommons.strings.string_conv import num_to_str
9from pycommons.types import type_error
11from moptipy.api.objective import Objective
12from moptipy.mock.utils import make_ordered_list, sample_from_attractors
13from moptipy.utils.logger import CSV_SEPARATOR, KeyValueLogSection
14from moptipy.utils.nputils import is_np_float, is_np_int
17class MockObjective(Objective):
18 """A mock-up of an objective function."""
20 def __init__(self,
21 is_int: bool = True,
22 lb: int | float = -inf,
23 ub: int | float = inf,
24 fmin: int | float | None = None,
25 fattractors: Iterable[int | float | None] | None = None,
26 fmax: int | float | None = None,
27 seed: int | None = None) -> None:
28 """
29 Create a mock objective function.
31 :param is_int: is this objective function always integer?
32 :param lb: the lower bound
33 :param ub: the upper bound
34 :param fmin: the minimum value this objective actually takes on
35 :param fattractors: the attractor points
36 :param fmax: the maximum value this objective actually takes on
37 """
38 if not isinstance(is_int, bool):
39 raise type_error(is_int, "is_int", bool)
40 #: is this objective integer?
41 self.is_int: Final[bool] = is_int
43 if seed is None:
44 seed = int(default_rng().integers(0, 1 << 63))
45 elif not isinstance(seed, int):
46 raise type_error(seed, "seed", int)
47 #: the random seed
48 self.seed: Final[int] = seed
50 #: the generator for setting up the mock objective
51 random: Final[Generator] = default_rng(seed)
52 #: the name of this objective function
53 self.name: Final[str] = f"mock{random.integers(1, 100_000_000):x}"
55 if not isinstance(lb, int | float):
56 raise type_error(lb, "lb", (int, float))
57 if isfinite(lb) and is_int and not isinstance(lb, int):
58 raise type_error(lb, f"finite lb @ is_int={is_int}", int)
59 if not isinstance(ub, int | float):
60 raise type_error(ub, "ub", (int, float))
61 if isfinite(ub) and is_int and not isinstance(ub, int):
62 raise type_error(ub, f"finite lb @ is_int={is_int}", int)
63 if lb >= ub:
64 raise ValueError(f"lb={lb} >= ub={ub} not permitted")
65 #: the lower bound
66 self.lb: Final[int | float] = lb
67 #: the upper bound
68 self.ub: Final[int | float] = ub
70 if fmin is not None:
71 if not isinstance(fmin, int if is_int else (int, float)):
72 raise type_error(fmin, f"fmin[is_int={is_int}",
73 int if is_int else (int, float))
74 if fmin < lb:
75 raise ValueError(f"fmin={fmin} < lb={lb}")
76 if fmax is not None:
77 if not isinstance(fmax, int if is_int else (int, float)):
78 raise type_error(fmax, f"fmax[is_int={is_int}",
79 int if is_int else (int, float))
80 if fmax > ub:
81 raise ValueError(f"fmax={fmax} < ub={ub}")
82 if (fmin is not None) and (fmax is not None) and (fmin >= fmax):
83 raise ValueError(f"fmin={fmin} >= fmax={fmax}")
85 values: list[int | float | None] = [lb, fmin]
86 if fattractors is None:
87 while True:
88 values.append(None)
89 if random.integers(2) <= 0:
90 break
91 else:
92 values.extend(fattractors)
93 values.extend((fmax, ub))
95 values = make_ordered_list(values, is_int, random)
96 if values is None:
97 raise ValueError(
98 f"could not create mock objective with lb={lb}, fmin={fmin}, "
99 f"fattractors={fattractors}, fmax={fmax}, ub={ub}, "
100 f"is_int={is_int}, and seed={seed}")
102 #: the minimum value the function actually takes on
103 self.fmin: Final[int | float] = values[1]
104 #: the maximum value the function actually takes on
105 self.fmax: Final[int | float] = values[-2]
106 #: the mean value the function actually takes on
107 self.fattractors: Final[tuple[int | float, ...]] =\
108 cast("tuple[int | float, ...]", tuple(values[2:-2]))
109 #: the internal random number generator
110 self.__random: Final[Generator] = random
112 def sample(self) -> int | float:
113 """
114 Sample the mock objective function.
116 :returns: the value of the mock objective function
117 """
118 return sample_from_attractors(self.__random, self.fattractors,
119 self.is_int, self.lb, self.ub)
121 def evaluate(self, x) -> float | int:
122 """
123 Return a mock objective value.
125 :param x: the candidate solution
126 :return: the objective value
127 """
128 seed: int | None = None
129 if hasattr(x, "__hash__") and (x.__hash__ is not None):
130 seed = hash(x)
131 elif isinstance(x, np.ndarray):
132 seed = hash(x.tobytes())
133 elif isinstance(x, list):
134 seed = hash(str(x))
135 random = self.__random if seed is None else default_rng(abs(seed))
137 return sample_from_attractors(random, self.fattractors,
138 self.is_int, self.lb, self.ub)
140 def lower_bound(self) -> float | int:
141 """
142 Get the lower bound of the objective value.
144 :return: the lower bound of the objective value
145 """
146 return self.lb
148 def upper_bound(self) -> float | int:
149 """
150 Get the upper bound of the objective value.
152 :return: the upper bound of the objective value
153 """
154 return self.ub
156 def is_always_integer(self) -> bool:
157 """
158 Return `True` if :meth:`~evaluate` will always return an `int` value.
160 :returns: `True` if :meth:`~evaluate` will always return an `int`
161 or `False` if also a `float` may be returned.
162 """
163 return self.is_int
165 def __str__(self):
166 """Get the name of this mock objective function."""
167 return self.name
169 def log_parameters_to(self, logger: KeyValueLogSection) -> None:
170 """Log the special parameters of tis mock objective function."""
171 super().log_parameters_to(logger)
172 logger.key_value("min", self.fmin)
173 logger.key_value("attractors", CSV_SEPARATOR.join([
174 num_to_str(n) for n in self.fattractors]))
175 logger.key_value("max", self.fmax)
176 logger.key_value("seed", self.seed)
177 logger.key_value("is_int", self.is_int)
179 @staticmethod
180 def for_type(dtype: np.dtype) -> "MockObjective":
181 """
182 Create a mock objective function with values bound by a given `dtype`.
184 :param dtype: the numpy data type
185 :returns: the mock objective function
186 """
187 if not isinstance(dtype, np.dtype):
188 raise type_error(dtype, "dtype", np.dtype)
190 random = default_rng()
191 params: dict[str, Any] = {}
192 use_min = bool(random.integers(2) <= 0)
193 use_max = bool(random.integers(2) <= 0)
194 if not (use_min or use_max):
195 if random.integers(5) <= 0:
196 use_min = True
197 else:
198 use_max = True
199 if is_np_int(dtype):
200 params["is_int"] = True
201 iix = np.iinfo(cast("Any", dtype))
202 params["lb"] = lbi = max(int(iix.min), -(1 << 58))
203 params["ub"] = ubi = min(int(iix.max), (1 << 58))
204 if use_min:
205 params["fmin"] = lbi + 1
206 if use_max:
207 params["fmax"] = ubi - 1
209 if is_np_float(dtype):
210 params["is_int"] = False
211 fix = np.finfo(dtype)
212 params["lb"] = lbf = max(float(fix.min), -1e300)
213 params["ub"] = ubf = min(float(fix.max), 1e300)
214 if use_min:
215 params["fmin"] = nextafter(float(lbf + float(fix.eps)), inf)
216 if use_max:
217 params["fmax"] = nextafter(float(ubf - float(fix.eps)), -inf)
219 if len(params) > 0:
220 return MockObjective(**params)
221 raise ValueError(f"unsupported dtype: {dtype}")