Source code for evokit.core.evaluator

from __future__ import annotations

from abc import ABC, ABCMeta, abstractmethod
from typing import TYPE_CHECKING, Generic, TypeVar
from functools import wraps
from typing import override

from .population import Individual

if TYPE_CHECKING:
    from typing import Self
    from typing import Type
    from typing import Tuple
    from typing import Dict
    from typing import Callable
    from .population import Population

from typing import Any


D = TypeVar("D", bound=Individual)


class MetaEvaluator(ABCMeta):
    """Machinery. Implement special behaviours in :class:`Evaluator`.

    :meta private:
    """
    # ^^ Actually a private metaclass! :meta private: indeed.
    def __new__(mcls: Type, name: str, bases: Tuple[type],
                namespace: Dict[str, Any]) -> Any:  # BAD
        ABCMeta.__init__(mcls, name, bases, namespace)
        # Remorseless metaclass abuse. Consider using __init_subclass__ instead.
        # This bad boy violates so many OO practices. Everything for ease
        #   of use, I guess.

        def wrap_function(custom_evaluate: Callable[[Any, Any],
                                                    tuple[float, ...]]) -> Callable:
            @wraps(custom_evaluate)
            def wrapper(self: Evaluator, individual: Individual,
                        *args: Any, **kwargs: Any) -> tuple[float, ...]:
                if not isinstance(individual, Individual):
                    raise TypeError("The input is not an individual")
                # If :attr:`retain_fitness` and the individual is scored, then
                #   return that score. Otherwise, evaluate the individual.
                if (self.retain_fitness and individual.has_fitness()):
                    return individual.fitness
                else:
                    return custom_evaluate(self, individual, *args, **kwargs)
            return wrapper

        namespace["evaluate"] = wrap_function(
            namespace.setdefault("evaluate", lambda: None)
        )
        return type.__new__(mcls, name, bases, namespace)


[docs] class Evaluator(ABC, Generic[D], metaclass=MetaEvaluator): """Base class for all evaluators. Derive this class to create custom evaluators. Tutorial: :doc:`../guides/examples/onemax`. """
[docs] def __new__(cls, *args: Any, **kwargs: Any) -> Self: """Machinery. Implement managed attributes. :meta private: """ instance = super().__new__(cls) instance.retain_fitness = False return instance
[docs] def __init__(self: Self) -> None: self.retain_fitness: bool """ If this evaluator should re-evaluate an :class:`Individual` whose :attr:`.fitness` is already set. """
[docs] @abstractmethod def evaluate(self: Self, individual: D) -> tuple[float, ...]: """Evaluation strategy. Return the fitness of an individual. Subclasses should override this method. Note: The implementation should assign higher fitness to better individuals. Args: individual: individual to evaluate """
[docs] def evaluate_population(self: Self, pop: Population[D]) -> None: """Context of :meth:`evaluate`. Iterate individuals in a population. For each individual, compute a fitness with :meth:`evaluate`, then assign that value to the individual. A subclass may override this method to implement behaviours that require access to the entire population. Effect: For each item in :arg:`pop`, set its :attr:`.fitness .Individual.fitness`. Note: This method must **never** return a value. It must assign to :attr:`.fitness` for each :class:`.Individual` in the :class:`.Population`. The result must be sorted, so that the earliest item has the highest fitness. """ for x in pop: x.fitness = self.evaluate(x) pop.sort()
[docs] class NullEvaluator(Evaluator[Any]):
[docs] @override def evaluate(self: Self, _: Any) -> Tuple[float]: return (0,)