Source code for moptipy.api.mo_archive
"""An archive of solutions to a multi-objective problems."""
from typing import Any, Final
import numpy as np
from pycommons.types import type_error
from moptipy.api.component import Component
from moptipy.api.mo_utils import lexicographic
from moptipy.utils.nputils import array_to_str
[docs]
class MORecord:
"""
A record for the multi-objective archive.
The default sorting order of multi-objective records is lexicographic
based on the objective value vector.
>>> import numpy as npx
>>> mr1 = MORecord("xxx", npx.array([1, 2, 3], int))
>>> print(mr1.x)
xxx
>>> print(mr1.fs)
[1 2 3]
>>> print(mr1)
fs=1;2;3, x=xxx
>>> mr2 = MORecord("yyy", npx.array([1, 2, 1], int))
>>> print(mr2.x)
yyy
>>> print(mr2.fs)
[1 2 1]
>>> mr1 < mr2
False
>>> mr2 < mr1
True
"""
def __init__(self, x: Any, fs: np.ndarray) -> None:
"""
Create a multi-objective record.
:param x: the point in the search space
:param fs: the vector of objective values
"""
if x is None:
raise TypeError("x must not be None")
#: the point in the search space
self.x: Final[Any] = x
if not isinstance(fs, np.ndarray):
raise type_error(fs, "fs", np.ndarray)
#: the vector of objective values
self.fs: Final[np.ndarray] = fs
def __lt__(self, other) -> bool:
"""
Compare for sorting.
:param other: the other record
>>> import numpy as npx
>>> r1 = MORecord("a", npx.array([1, 1, 1]))
>>> r2 = MORecord("b", npx.array([1, 1, 1]))
>>> r1 < r2
False
>>> r2 < r1
False
>>> r2 = MORecord("b", npx.array([1, 1, 2]))
>>> r1 < r2
True
>>> r2 < r1
False
>>> r1 = MORecord("a", npx.array([2, 1, 1]))
>>> r1 < r2
False
>>> r2 < r1
True
"""
return lexicographic(self.fs, other.fs) < 0
def __str__(self):
"""
Get the string representation of this record.
:returns: the string representation of this record
>>> import numpy as npx
>>> r = MORecord(4, npx.array([1, 2, 3]))
>>> print(r)
fs=1;2;3, x=4
"""
return f"fs={array_to_str(self.fs)}, x={self.x}"
[docs]
class MOArchivePruner(Component):
"""A strategy for pruning an archive of solutions."""
[docs]
def prune(self, archive: list[MORecord], n_keep: int, size: int) -> None:
"""
Prune an archive.
After invoking this method, the first `n_keep` entries in `archive`
are selected to be preserved. The remaining entries
(at indices `n_keep...len(archive)-1`) can be deleted.
Pruning therefore is basically just a method of sorting the archive
according to a preference order of solutions. It will not delete any
element from the list. The caller can do that afterwards if she wants.
This base class just provides a simple FIFO scheme.
:param archive: the archive, i.e., a list of tuples of solutions and
their objective vectors
:param n_keep: the number of solutions to keep
:param size: the current size of the archive
"""
if size > n_keep:
n_delete: Final[int] = size - n_keep
move_to_end: Final[list[MORecord]] = archive[:n_delete]
archive[0:n_keep] = archive[n_delete:size]
archive[size - n_delete:size] = move_to_end
def __str__(self):
"""
Get the name of this archive pruning strategy.
:returns: the name of this archive pruning strategy
"""
return "fifo"
[docs]
def check_mo_archive_pruner(pruner: Any) -> MOArchivePruner:
"""
Check whether an object is a valid instance of :class:`MOArchivePruner`.
:param pruner: the multi-objective archive pruner
:return: the object
:raises TypeError: if `pruner` is not an instance of
:class:`MOArchivePruner`
>>> check_mo_archive_pruner(MOArchivePruner())
fifo
>>> try:
... check_mo_archive_pruner('A')
... except TypeError as te:
... print(te)
pruner should be an instance of moptipy.api.mo_archive.\
MOArchivePruner but is str, namely 'A'.
>>> try:
... check_mo_archive_pruner(None)
... except TypeError as te:
... print(te)
pruner should be an instance of moptipy.api.mo_archive.\
MOArchivePruner but is None.
"""
if isinstance(pruner, MOArchivePruner):
return pruner
raise type_error(pruner, "pruner", MOArchivePruner)