"""A mock-up of an objective function."""
from math import inf, isfinite, nextafter
from typing import Any, Final, Iterable, cast
import numpy as np
from numpy.random import Generator, default_rng
from pycommons.strings.string_conv import num_to_str
from pycommons.types import type_error
from moptipy.api.objective import Objective
from moptipy.mock.utils import make_ordered_list, sample_from_attractors
from moptipy.utils.logger import CSV_SEPARATOR, KeyValueLogSection
from moptipy.utils.nputils import is_np_float, is_np_int
[docs]
class MockObjective(Objective):
"""A mock-up of an objective function."""
def __init__(self,
is_int: bool = True,
lb: int | float = -inf,
ub: int | float = inf,
fmin: int | float | None = None,
fattractors: Iterable[int | float | None] | None = None,
fmax: int | float | None = None,
seed: int | None = None) -> None:
"""
Create a mock objective function.
:param is_int: is this objective function always integer?
:param lb: the lower bound
:param ub: the upper bound
:param fmin: the minimum value this objective actually takes on
:param fattractors: the attractor points
:param fmax: the maximum value this objective actually takes on
"""
if not isinstance(is_int, bool):
raise type_error(is_int, "is_int", bool)
#: is this objective integer?
self.is_int: Final[bool] = is_int
if seed is None:
seed = int(default_rng().integers(0, 1 << 63))
elif not isinstance(seed, int):
raise type_error(seed, "seed", int)
#: the random seed
self.seed: Final[int] = seed
#: the generator for setting up the mock objective
random: Final[Generator] = default_rng(seed)
#: the name of this objective function
self.name: Final[str] = \
f"mock{hex(random.integers(1, 100_000_000))[2:]}"
if not isinstance(lb, int | float):
raise type_error(lb, "lb", (int, float))
if isfinite(lb) and is_int and not isinstance(lb, int):
raise type_error(lb, f"finite lb @ is_int={is_int}", int)
if not isinstance(ub, int | float):
raise type_error(ub, "ub", (int, float))
if isfinite(ub) and is_int and not isinstance(ub, int):
raise type_error(ub, f"finite lb @ is_int={is_int}", int)
if lb >= ub:
raise ValueError(f"lb={lb} >= ub={ub} not permitted")
#: the lower bound
self.lb: Final[int | float] = lb
#: the upper bound
self.ub: Final[int | float] = ub
if fmin is not None:
if not isinstance(fmin, int if is_int else (int, float)):
raise type_error(fmin, f"fmin[is_int={is_int}",
int if is_int else (int, float))
if fmin < lb:
raise ValueError(f"fmin={fmin} < lb={lb}")
if fmax is not None:
if not isinstance(fmax, int if is_int else (int, float)):
raise type_error(fmax, f"fmax[is_int={is_int}",
int if is_int else (int, float))
if fmax > ub:
raise ValueError(f"fmax={fmax} < ub={ub}")
if (fmin is not None) and (fmax is not None) and (fmin >= fmax):
raise ValueError(f"fmin={fmin} >= fmax={fmax}")
values: list[int | float | None] = [lb, fmin]
if fattractors is None:
while True:
values.append(None)
if random.integers(2) <= 0:
break
else:
values.extend(fattractors)
values.append(fmax)
values.append(ub)
values = make_ordered_list(values, is_int, random)
if values is None:
raise ValueError(
f"could not create mock objective with lb={lb}, fmin={fmin}, "
f"fattractors={fattractors}, fmax={fmax}, ub={ub}, "
f"is_int={is_int}, and seed={seed}")
#: the minimum value the function actually takes on
self.fmin: Final[int | float] = values[1]
#: the maximum value the function actually takes on
self.fmax: Final[int | float] = values[-2]
#: the mean value the function actually takes on
self.fattractors: Final[tuple[int | float, ...]] =\
cast(tuple[int | float, ...], tuple(values[2:-2]))
#: the internal random number generator
self.__random: Final[Generator] = random
[docs]
def sample(self) -> int | float:
"""
Sample the mock objective function.
:returns: the value of the mock objective function
"""
return sample_from_attractors(self.__random, self.fattractors,
self.is_int, self.lb, self.ub)
[docs]
def evaluate(self, x) -> float | int:
"""
Return a mock objective value.
:param x: the candidate solution
:return: the objective value
"""
seed: int | None = None
if hasattr(x, "__hash__") and (x.__hash__ is not None):
seed = hash(x)
elif isinstance(x, np.ndarray):
seed = hash(x.tobytes())
elif isinstance(x, list):
seed = hash(str(x))
random = self.__random if seed is None else default_rng(abs(seed))
return sample_from_attractors(random, self.fattractors,
self.is_int, self.lb, self.ub)
[docs]
def lower_bound(self) -> float | int:
"""
Get the lower bound of the objective value.
:return: the lower bound of the objective value
"""
return self.lb
[docs]
def upper_bound(self) -> float | int:
"""
Get the upper bound of the objective value.
:return: the upper bound of the objective value
"""
return self.ub
[docs]
def is_always_integer(self) -> bool:
"""
Return `True` if :meth:`~evaluate` will always return an `int` value.
:returns: `True` if :meth:`~evaluate` will always return an `int`
or `False` if also a `float` may be returned.
"""
return self.is_int
def __str__(self):
"""Get the name of this mock objective function."""
return self.name
[docs]
def log_parameters_to(self, logger: KeyValueLogSection) -> None:
"""Log the special parameters of tis mock objective function."""
super().log_parameters_to(logger)
logger.key_value("min", self.fmin)
logger.key_value("attractors", CSV_SEPARATOR.join([
num_to_str(n) for n in self.fattractors]))
logger.key_value("max", self.fmax)
logger.key_value("seed", self.seed)
logger.key_value("is_int", self.is_int)
[docs]
@staticmethod
def for_type(dtype: np.dtype) -> "MockObjective":
"""
Create a mock objective function with values bound by a given `dtype`.
:param dtype: the numpy data type
:returns: the mock objective function
"""
if not isinstance(dtype, np.dtype):
raise type_error(dtype, "dtype", np.dtype)
random = default_rng()
params: dict[str, Any] = {}
use_min = random.integers(2) <= 0
use_max = random.integers(2) <= 0
if not (use_min or use_max):
if random.integers(5) <= 0:
use_min = True
else:
use_max = True
if is_np_int(dtype):
params["is_int"] = True
ii = np.iinfo(dtype)
params["lb"] = lbi = max(int(ii.min), -(1 << 58))
params["ub"] = ubi = min(int(ii.max), (1 << 58))
if use_min:
params["fmin"] = lbi + 1
if use_max:
params["fmax"] = ubi - 1
if is_np_float(dtype):
params["is_int"] = False
fi = np.finfo(dtype)
params["lb"] = lbf = max(float(fi.min), -1e300)
params["ub"] = ubf = min(float(fi.max), 1e300)
if use_min:
params["fmin"] = nextafter(float(lbf + fi.eps), inf)
if use_max:
params["fmax"] = nextafter(float(ubf - fi.eps), -inf)
if len(params) > 0:
return MockObjective(**params)
raise ValueError(f"unsupported dtype: {dtype}")