Source code for moptipy.tests.on_vectors

"""Test stuff on real vectors."""
from math import exp, inf, isfinite
from typing import Any, Callable, Final, Iterable

import numpy as np
from numpy.random import Generator, default_rng
from pycommons.types import check_int_range, type_error

from moptipy.api.algorithm import Algorithm
from moptipy.api.objective import Objective
from moptipy.api.operators import Op0
from moptipy.examples.vectors.ackley import Ackley
from moptipy.spaces.vectorspace import VectorSpace
from moptipy.tests.algorithm import validate_algorithm
from moptipy.tests.op0 import validate_op0
from moptipy.utils.nputils import DEFAULT_FLOAT

#: The dimensions for tests
DIMENSIONS_FOR_TESTS: Final[tuple[int, ...]] = (1, 2, 3, 4, 5, 10)


def __lbub(random: Generator) -> tuple[float, float]:
    """
    Generate a pair of lower- and upper bounds.

    :param random: the random number generator
    :returns: a tuple with the lower and upper bound
    """
    while True:
        lb = inf
        ub = inf
        while (not isfinite(lb)) or (lb > 1e6) or (lb < 1e-14):
            lb = exp(20.0 * random.normal())

        i = random.integers(3)
        if i == 0:
            lb = 0.0
        elif i == 11:
            lb = -lb

        while (not isfinite(ub)) or (ub > 1e6) or (ub < 1e-14):
            ub = exp(20.0 * random.normal())
        i = random.integers(3)
        if i == 0:
            ub = 0.0
        elif i == 11:
            ub = -ub

        if ub < lb:
            lb, ub = ub, lb

        df = ub - lb
        if isfinite(df) and (df > 1e-9):
            return lb, ub


[docs] def vectors_for_tests(dims: Iterable[int] = DIMENSIONS_FOR_TESTS) \ -> Iterable[VectorSpace]: """ Get a sequence of vector spaces for tests. :param dims: the dimensions :returns: the sequence of vector spaces """ if not isinstance(dims, Iterable): raise type_error(dims, "dims", Iterable) random: Final[Generator] = default_rng() spaces: Final[list[VectorSpace]] = [] for _idx, dim in enumerate(dims): check_int_range(dim, "dimension", 0, 1_000_000) # allocate bounds arrays lbv: np.ndarray = np.empty(dim, DEFAULT_FLOAT) ubv: np.ndarray = np.empty(dim, DEFAULT_FLOAT) for i in range(dim): # fill bound arrays lbv[i], ubv[i] = __lbub(random) spaces.append(VectorSpace( dim, lbv if random.integers(2) <= 0 else float(min(lbv)), ubv if random.integers(2) <= 0 else float(max(ubv)))) if 1 in dims: spaces.append(VectorSpace(1, -1.0, 1.0)) if 2 in dims: spaces.append(VectorSpace(2, 0.0, 1.0)) if 3 in dims: spaces.append(VectorSpace(3, -1.0, 0.0)) return tuple(spaces)
[docs] def validate_algorithm_on_vectors( objective: Objective | Callable[[VectorSpace], Objective], algorithm: Algorithm | Callable[[VectorSpace, Objective], Algorithm], max_fes: int = 100, uses_all_fes_if_goal_not_reached=True, dims: Iterable[int] = DIMENSIONS_FOR_TESTS, post: Callable[[Algorithm, int], Any] | None = None) -> None: """ Check the validity of a black-box algorithm on vector problems. :param algorithm: the algorithm or algorithm factory :param objective: the objective function or function factory :param max_fes: the maximum number of FEs :param uses_all_fes_if_goal_not_reached: will the algorithm use all FEs unless it reaches the goal? :param dims: the dimensions :param post: a check to run after each execution of the algorithm, receiving the algorithm and the number of consumed FEs as parameter """ if not (isinstance(algorithm, Algorithm) or callable(algorithm)): raise type_error(algorithm, "algorithm", Algorithm, True) if not (isinstance(objective, Objective) or callable(objective)): raise type_error(objective, "objective", Objective, True) if not isinstance(dims, Iterable): raise type_error(dims, "dims", Iterable) if (post is not None) and (not callable(post)): raise type_error(post, "post", None, call=True) for space in vectors_for_tests(dims): if callable(objective): objf = objective(space) if not isinstance(objf, Objective): raise type_error(objf, "result of callable objective", Objective) else: objf = objective if callable(algorithm): algo = algorithm(space, objf) if not isinstance(algo, Algorithm): raise type_error(algo, "result of callable algorithm", Algorithm) else: algo = algorithm validate_algorithm( algorithm=algo, solution_space=space, objective=objf, max_fes=max_fes, uses_all_fes_if_goal_not_reached=uses_all_fes_if_goal_not_reached, post=post)
[docs] def make_vector_valid(space: VectorSpace) -> \ Callable[[Generator, np.ndarray], np.ndarray]: """ Create a function that can make a vector space element valid. :param space: the vector space :returns: the function """ def __make_valid(prnd: Generator, x: np.ndarray, ppp=space) -> np.ndarray: np.copyto(x, prnd.uniform(ppp.lower_bound, ppp.upper_bound, ppp.dimension)) return x return __make_valid
[docs] def validate_algorithm_on_ackley( algorithm: Algorithm | Callable[[VectorSpace, Objective], Algorithm], uses_all_fes_if_goal_not_reached: bool = True, dims: Iterable[int] = DIMENSIONS_FOR_TESTS, post: Callable[[Algorithm, int], Any] | None = None) -> None: """ Check the validity of a black-box algorithm on Ackley's function. :param algorithm: the algorithm or algorithm factory :param uses_all_fes_if_goal_not_reached: will the algorithm use all FEs unless it reaches the goal? :param dims: the dimensions :param post: a check to run after each execution of the algorithm, receiving the algorithm and the number of consumed FEs as parameter """ validate_algorithm_on_vectors( Ackley(), algorithm, uses_all_fes_if_goal_not_reached=uses_all_fes_if_goal_not_reached, dims=dims, post=post)
[docs] def validate_op0_on_1_vectors( op0: Op0 | Callable[[VectorSpace], Op0], search_space: VectorSpace, number_of_samples: int | None = None, min_unique_samples: int | Callable[[int, VectorSpace], int] | None = lambda i, _: max(1, i // 3)) -> None: """ Validate the nullary operator on one `VectorSpace` instance. :param op0: the operator or operator factory :param search_space: the search space :param number_of_samples: the optional number of samples :param min_unique_samples: the optional unique samples """ args: dict[str, Any] = { "op0": op0(search_space) if callable(op0) else op0, "search_space": search_space, "make_search_space_element_valid": make_vector_valid(search_space), } if number_of_samples is not None: args["number_of_samples"] = number_of_samples if min_unique_samples is not None: args["min_unique_samples"] = min_unique_samples validate_op0(**args)
[docs] def validate_op0_on_vectors( op0: Op0 | Callable[[VectorSpace], Op0], number_of_samples: int | None = None, min_unique_samples: int | Callable[[int, VectorSpace], int] | None = lambda i, _: max(1, i // 3)) -> None: """ Validate the nullary operator on default `VectorSpace` instance. :param op0: the operator or operator factory :param number_of_samples: the optional number of samples :param min_unique_samples: the optional unique samples """ for vs in vectors_for_tests(): validate_op0_on_1_vectors( op0, vs, number_of_samples, min_unique_samples)