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

1"""An archive of solutions to a multi-objective problems.""" 

2 

3from typing import Any, Final 

4 

5import numpy as np 

6from pycommons.types import type_error 

7 

8from moptipy.api.component import Component 

9from moptipy.api.mo_utils import lexicographic 

10from moptipy.utils.nputils import array_to_str 

11 

12 

13class MORecord: 

14 """ 

15 A record for the multi-objective archive. 

16 

17 The default sorting order of multi-objective records is lexicographic 

18 based on the objective value vector. 

19 

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 """ 

38 

39 def __init__(self, x: Any, fs: np.ndarray) -> None: 

40 """ 

41 Create a multi-objective record. 

42 

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 

54 

55 def __lt__(self, other) -> bool: 

56 """ 

57 Compare for sorting. 

58 

59 :param other: the other record 

60 

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 

80 

81 def __str__(self): 

82 """ 

83 Get the string representation of this record. 

84 

85 :returns: the string representation of this record 

86 

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}" 

93 

94 

95class MOArchivePruner(Component): 

96 """A strategy for pruning an archive of solutions.""" 

97 

98 def prune(self, archive: list[MORecord], n_keep: int, size: int) -> None: 

99 """ 

100 Prune an archive. 

101 

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. 

105 

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. 

109 

110 This base class just provides a simple FIFO scheme. 

111 

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 

122 

123 def __str__(self): 

124 """ 

125 Get the name of this archive pruning strategy. 

126 

127 :returns: the name of this archive pruning strategy 

128 """ 

129 return "fifo" 

130 

131 

132def check_mo_archive_pruner(pruner: Any) -> MOArchivePruner: 

133 """ 

134 Check whether an object is a valid instance of :class:`MOArchivePruner`. 

135 

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` 

140 

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)