Coverage for moptipy / api / mo_archive.py: 94%
31 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
1"""An archive of solutions to a multi-objective problems."""
3from typing import Any, Final
5import numpy as np
6from pycommons.types import type_error
8from moptipy.api.component import Component
9from moptipy.api.mo_utils import lexicographic
10from moptipy.utils.nputils import array_to_str
13class MORecord:
14 """
15 A record for the multi-objective archive.
17 The default sorting order of multi-objective records is lexicographic
18 based on the objective value vector.
20 >>> import numpy as npx
21 >>> mr1 = MORecord("xxx", npx.array([1, 2, 3], int))
22 >>> print(mr1.x)
23 xxx
24 >>> print(mr1.fs)
25 [1 2 3]
26 >>> print(mr1)
27 fs=1;2;3, x=xxx
28 >>> mr2 = MORecord("yyy", npx.array([1, 2, 1], int))
29 >>> print(mr2.x)
30 yyy
31 >>> print(mr2.fs)
32 [1 2 1]
33 >>> mr1 < mr2
34 False
35 >>> mr2 < mr1
36 True
37 """
39 def __init__(self, x: Any, fs: np.ndarray) -> None:
40 """
41 Create a multi-objective record.
43 :param x: the point in the search space
44 :param fs: the vector of objective values
45 """
46 if x is None:
47 raise TypeError("x must not be None")
48 #: the point in the search space
49 self.x: Final[Any] = x
50 if not isinstance(fs, np.ndarray):
51 raise type_error(fs, "fs", np.ndarray)
52 #: the vector of objective values
53 self.fs: Final[np.ndarray] = fs
55 def __lt__(self, other) -> bool:
56 """
57 Compare for sorting.
59 :param other: the other record
61 >>> import numpy as npx
62 >>> r1 = MORecord("a", npx.array([1, 1, 1]))
63 >>> r2 = MORecord("b", npx.array([1, 1, 1]))
64 >>> r1 < r2
65 False
66 >>> r2 < r1
67 False
68 >>> r2 = MORecord("b", npx.array([1, 1, 2]))
69 >>> r1 < r2
70 True
71 >>> r2 < r1
72 False
73 >>> r1 = MORecord("a", npx.array([2, 1, 1]))
74 >>> r1 < r2
75 False
76 >>> r2 < r1
77 True
78 """
79 return lexicographic(self.fs, other.fs) < 0
81 def __str__(self):
82 """
83 Get the string representation of this record.
85 :returns: the string representation of this record
87 >>> import numpy as npx
88 >>> r = MORecord(4, npx.array([1, 2, 3]))
89 >>> print(r)
90 fs=1;2;3, x=4
91 """
92 return f"fs={array_to_str(self.fs)}, x={self.x}"
95class MOArchivePruner(Component):
96 """A strategy for pruning an archive of solutions."""
98 def prune(self, archive: list[MORecord], n_keep: int, size: int) -> None:
99 """
100 Prune an archive.
102 After invoking this method, the first `n_keep` entries in `archive`
103 are selected to be preserved. The remaining entries
104 (at indices `n_keep...len(archive)-1`) can be deleted.
106 Pruning therefore is basically just a method of sorting the archive
107 according to a preference order of solutions. It will not delete any
108 element from the list. The caller can do that afterwards if she wants.
110 This base class just provides a simple FIFO scheme.
112 :param archive: the archive, i.e., a list of tuples of solutions and
113 their objective vectors
114 :param n_keep: the number of solutions to keep
115 :param size: the current size of the archive
116 """
117 if size > n_keep:
118 n_delete: Final[int] = size - n_keep
119 move_to_end: Final[list[MORecord]] = archive[:n_delete]
120 archive[0:n_keep] = archive[n_delete:size]
121 archive[size - n_delete:size] = move_to_end
123 def __str__(self):
124 """
125 Get the name of this archive pruning strategy.
127 :returns: the name of this archive pruning strategy
128 """
129 return "fifo"
132def check_mo_archive_pruner(pruner: Any) -> MOArchivePruner:
133 """
134 Check whether an object is a valid instance of :class:`MOArchivePruner`.
136 :param pruner: the multi-objective archive pruner
137 :return: the object
138 :raises TypeError: if `pruner` is not an instance of
139 :class:`MOArchivePruner`
141 >>> check_mo_archive_pruner(MOArchivePruner())
142 fifo
143 >>> try:
144 ... check_mo_archive_pruner('A')
145 ... except TypeError as te:
146 ... print(te)
147 pruner should be an instance of moptipy.api.mo_archive.\
148MOArchivePruner but is str, namely 'A'.
149 >>> try:
150 ... check_mo_archive_pruner(None)
151 ... except TypeError as te:
152 ... print(te)
153 pruner should be an instance of moptipy.api.mo_archive.\
154MOArchivePruner but is None.
155 """
156 if isinstance(pruner, MOArchivePruner):
157 return pruner
158 raise type_error(pruner, "pruner", MOArchivePruner)