Source code for moptipy.api.algorithm

"""
The base classes for implementing optimization algorithms.

All optimization algorithms implemented based on the `moptipy` API inherit
from :class:`~moptipy.api.algorithm.Algorithm`. If you implement a new
algorithm, you will want to override the following methods:

1. :meth:`~moptipy.api.algorithm.Algorithm.solve` implements the algorithm
   itself. It receives an instance of :class:`~moptipy.api.process.Process` as
   parameter that allows for the creation and evaluation of candidate
   solutions and that provides a random number generator. The optimization
   algorithm then will sample solutions and pass them to
   :meth:`~moptipy.api.process.Process.evaluate` to obtain their objective
   value, striving sampling better and better solutions.
2. The dunder method `__str__` should be overridden to return a short mnemonic
   name of the algorithm.
3. :meth:`~moptipy.api.component.Component.log_parameters_to` needs to be
   overridden if the algorithm has any parameters. This methods then should
   store the values of all the parameters to the logging context. It should
   also invoke the :meth:`~moptipy.api.component.Component.log_parameters_to`
   routines of all sub-components of the algorithm.
4. :meth:`~moptipy.api.component.Component.initialize` needs to be overridden
   to reset/initialize all internal data structures and to invoke all the
   :meth:`~moptipy.api.component.Component.initialize` of all components (such
   as search operators) of the algorithm.

Notice that we already provide specialized algorithm sub-classes for several
common scenarios, such as:

1. :class:`~moptipy.api.algorithm.Algorithm0` for algorithms that have a
   nullary search operator (:class:`~moptipy.api.operators.Op0`).
2. :class:`~moptipy.api.algorithm.Algorithm1` for algorithms that have  a
   nullary (:class:`~moptipy.api.operators.Op0`) and an unary
   (:class:`~moptipy.api.operators.Op1`) search operator.
3. :class:`~moptipy.api.algorithm.Algorithm2` for algorithms that have  a
   nullary (:class:`~moptipy.api.operators.Op0`), an unary
   (:class:`~moptipy.api.operators.Op1`), and a binary
   (:class:`~moptipy.api.operators.Op2`) search operator.
4. :class:`~moptipy.api.mo_algorithm.MOAlgorithm` for multi-objective
   optimization problems.

These classes automatically invoke the
:meth:`~moptipy.api.component.Component.log_parameters_to` and
:meth:`~moptipy.api.component.Component.initialize` routines of their
operators.

If you implement a new algorithm, you can and should test with the pre-defined
unit test routine :func:`~moptipy.tests.algorithm.validate_algorithm`, or its
specialized versions

1. for bit-string based search spaces based on
   :func:`~moptipy.tests.on_bitstrings.validate_algorithm_on_bitstrings`):
   a. :func:`~moptipy.tests.on_bitstrings.validate_algorithm_on_onemax`,
   b. :func:`~moptipy.tests.on_bitstrings.validate_algorithm_on_leadingones`
2. for the JSSP based on
   :func:`~moptipy.tests.on_jssp.validate_algorithm_on_1_jssp`:
   a. :func:`~moptipy.tests.on_jssp.validate_algorithm_on_jssp`
3. on real-valued vector search spaces based on
   :func:`~moptipy.tests.on_vectors.validate_algorithm_on_vectors`):
   a. :func:`~moptipy.tests.on_vectors.validate_algorithm_on_ackley`
"""
from typing import Any, Final

from pycommons.types import type_error

from moptipy.api.component import Component
from moptipy.api.logging import SCOPE_OP0, SCOPE_OP1, SCOPE_OP2
from moptipy.api.operators import (
    Op0,
    Op1,
    Op2,
    check_op0,
    check_op1,
    check_op2,
)
from moptipy.api.process import Process
from moptipy.utils.logger import KeyValueLogSection
from moptipy.utils.strings import PART_SEPARATOR


# start book
[docs] class Algorithm(Component): """A base class for implementing optimization algorithms."""
[docs] def solve(self, process: Process) -> None: """ Apply this optimization algorithm to the given process. :param process: the process which provides methods to access the search space, the termination criterion, and a source of randomness. It also wraps the objective function, remembers the best-so-far solution, and takes care of creating log files (if this is wanted). """
# end book
[docs] class Algorithm0(Algorithm): """An algorithm with a nullary search operator.""" def __init__(self, name: str, op0: Op0) -> None: """ Create the algorithm with nullary search operator. :param name: the name of the algorithm :param op0: the nullary search operator """ #: The nullary search operator. self.op0: Final[Op0] = check_op0(op0) if not isinstance(name, str): raise type_error(name, "name", str) if len(name) <= 0: raise ValueError(f"Algorithm name cannot be {name!r}.") #: the name of this optimization algorithm, which is also the return #: value of `__str__()` self.name: Final[str] = name def __str__(self) -> str: """ Get the name of the algorithm. :return: the name of the algorithm """ return self.name
[docs] def initialize(self) -> None: """Initialize the algorithm.""" super().initialize() self.op0.initialize()
[docs] def log_parameters_to(self, logger: KeyValueLogSection) -> None: """ Log the parameters of the algorithm to a logger. :param logger: the logger for the parameters """ super().log_parameters_to(logger) with logger.scope(SCOPE_OP0) as sc: self.op0.log_parameters_to(sc)
[docs] class Algorithm1(Algorithm0): """An algorithm with a unary search operator.""" def __init__(self, name: str, op0: Op0, op1: Op1) -> None: """ Create the algorithm with nullary and unary search operator. :param name: the name of the algorithm :param op0: the nullary search operator :param op1: the unary search operator """ super().__init__(name if op1.__class__ == Op1 else f"{name}{PART_SEPARATOR}{op1}", op0) #: The unary search operator. self.op1: Final[Op1] = check_op1(op1)
[docs] def initialize(self) -> None: """Initialize the algorithm.""" super().initialize() self.op1.initialize()
[docs] def log_parameters_to(self, logger: KeyValueLogSection) -> None: """ Log the parameters of the algorithm to a logger. :param logger: the logger for the parameters """ super().log_parameters_to(logger) with logger.scope(SCOPE_OP1) as sc: self.op1.log_parameters_to(sc)
[docs] class Algorithm2(Algorithm1): """An algorithm with a binary and unary operator.""" def __init__(self, name: str, op0: Op0, op1: Op1, op2: Op2) -> None: """ Create the algorithm with nullary, unary, and binary search operator. :param name: the name of the algorithm :param op0: the nullary search operator :param op1: the unary search operator :param op2: the binary search operator """ super().__init__( name if op2.__class__ is Op2 else f"{name}{PART_SEPARATOR}{op2}", op0, op1) #: The binary search operator. self.op2: Final[Op2] = check_op2(op2)
[docs] def initialize(self) -> None: """Initialize the algorithm.""" super().initialize() self.op2.initialize()
[docs] def log_parameters_to(self, logger: KeyValueLogSection) -> None: """ Log the parameters of the algorithm to a logger. :param logger: the logger for the parameters """ super().log_parameters_to(logger) with logger.scope(SCOPE_OP2) as sc: self.op2.log_parameters_to(sc)
[docs] def check_algorithm(algorithm: Any) -> Algorithm: """ Check whether an object is a valid instance of :class:`Algorithm`. :param algorithm: the algorithm object :return: the object :raises TypeError: if `algorithm` is not an instance of :class:`Algorithm` >>> check_algorithm(Algorithm()) Algorithm >>> try: ... check_algorithm('A') ... except TypeError as te: ... print(te) algorithm should be an instance of moptipy.api.algorithm.\ Algorithm but is str, namely 'A'. >>> try: ... check_algorithm(None) ... except TypeError as te: ... print(te) algorithm should be an instance of moptipy.api.algorithm.\ Algorithm but is None. """ if isinstance(algorithm, Algorithm): return algorithm raise type_error(algorithm, "algorithm", Algorithm)