Coverage for moptipy / api / space.py: 94%

18 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +0000

1""" 

2Provide the functionality to access search and solution spaces. 

3 

4A :class:`Space` is the abstraction of the data structures for solutions and 

5points in the search space that need to be generated, copied, and stored 

6during the optimization process. This allows us to develop black-box 

7algorithms while still being able to properly remember the best solutions, 

8storing them as text strings in log files, and to validate whether they are 

9correct. 

10 

11All search or solution spaces in `moptipy` inherit from :class:`Space`. If 

12you implement a new space, you should test it with the pre-defined unit test 

13routine :func:`~moptipy.tests.space.validate_space`. 

14 

15The following pre-defined spaces are currently available: 

16 

17- :class:`~moptipy.spaces.bitstrings.BitStrings`, the space of `n`-dimensional 

18 bit strings 

19- :class:`~moptipy.spaces.intspace.IntSpace`, a space of `n`-dimensional 

20 integer strings, where each element is between predefined inclusive bounds 

21 `min_value...max_value`. 

22- :class:`~moptipy.spaces.permutations.Permutations` is a special version of 

23 the :class:`~moptipy.spaces.intspace.IntSpace` where all elements are 

24 permutations of a base string 

25 :attr:`~moptipy.spaces.permutations.Permutations.blueprint`. This means that 

26 it can represent permutations both with and without repetitions. Depending 

27 on the base string, each element may occur an element-specific number of 

28 times. For the base string `(-1, -1, 2, 7, 7, 7)`, for example, `-1` may 

29 occur twice, `2` can occur once, and `7` three times. 

30- :class:`~moptipy.spaces.vectorspace.VectorSpace` is the space of 

31 `n`-dimensional floating point number vectors. 

32""" 

33 

34from typing import Any 

35 

36from pycommons.types import type_error 

37 

38from moptipy.api.component import Component 

39 

40 

41# start book 

42class Space(Component): 

43 """ 

44 A class to represent both search and solution spaces. 

45 

46 The space basically defines a container data structure and basic 

47 operations that we can apply to them. For example, a solution 

48 space contains all the possible solutions to an optimization 

49 problem. All of them are instances of one data structure. An 

50 optimization as well as a black-box process needs to be able to 

51 create and copy such objects. In order to store the solutions we 

52 found in a text file, we must further be able to translate them to 

53 strings. We should also be able to parse such strings. It is also 

54 important to detect whether two objects are the same and whether 

55 the contents of an object are valid. All of this functionality is 

56 offered by the `Space` class. 

57 """ 

58 

59 def create(self) -> Any: 

60 # end book 

61 """ 

62 Generate an instance of the data structure managed by the space. 

63 

64 The state/contents of this data structure are undefined. It may 

65 not pass the :meth:`validate` method. 

66 

67 :return: the new instance 

68 """ 

69 

70 def copy(self, dest, source) -> None: # +book 

71 """ 

72 Copy one instance of the data structure to another one. 

73 

74 Notice that the first parameter of this method is the destination, 

75 which will be overwritten by copying the contents of the second 

76 parameter over it. 

77 

78 :param dest: the destination data structure, 

79 whose contents will be overwritten with those from `source` 

80 :param source: the source data structure, which remains 

81 unchanged and whose contents will be copied to `dest` 

82 """ 

83 

84 def to_str(self, x) -> str: # +book 

85 """ 

86 Obtain a textual representation of an instance of the data structure. 

87 

88 This method should convert an element of the space to a string 

89 representation that is parseable by :meth:from_str: and should ideally 

90 not be too verbose. For example, when converting a list or array `x` 

91 of integers to a string, one could simply do 

92 `";".join([str(xx) for xx in x])`, which would convert it to a 

93 semicolon-separated list without any wasted space. 

94 

95 Notice that this method is used by the 

96 :class:`~moptipy.utils.logger.Logger` when storing the final 

97 optimization results in the log files in form of a 

98 :class:`~moptipy.utils.logger.TextLogSection` created via 

99 :meth:`~moptipy.utils.logger.Logger.text`. By implementing 

100 :meth:from_str: and :meth:to_str: as exact inverse of each other, you 

101 can thus ensure that you can always automatically load the results of 

102 your optimization runs from the log files created via 

103 :meth:`~moptipy.api.execution.Execution.set_log_file` of the 

104 :class:`~moptipy.api.execution.Execution` class. 

105 

106 :param x: the instance 

107 :return: the string representation of x 

108 """ 

109 

110 def from_str(self, text: str) -> Any: # +book 

111 """ 

112 Transform a string `text` to one element of the space. 

113 

114 This method should be implemented as inverse to :meth:to_str:. 

115 It should check the validity of the result before returning it. 

116 It may not always be possible to implement this method, but you 

117 should try. 

118 

119 :param text: the input string 

120 :return: the element in the space corresponding to `text` 

121 """ 

122 

123 def is_equal(self, x1, x2) -> bool: # +book 

124 """ 

125 Check if the contents of two instances of the data structure are equal. 

126 

127 :param x1: the first instance 

128 :param x2: the second instance 

129 :return: `True` if the contents are equal, `False` otherwise 

130 """ 

131 

132 def validate(self, x) -> None: # +book 

133 """ 

134 Check whether a given point in the space is valid. 

135 

136 This function should be implemented such that it very carefully checks 

137 whether the argument `x` is a valid element of this space. It should 

138 check the Python data type of `x` and the type of its components and 

139 raise a `TypeError` if it does not match the exact requirements. It 

140 should also check the value of each element of `x` whether it is 

141 permitted. Once this function returns without throwing an exception, 

142 the user can rely on that the data structure `x` is correct. 

143 

144 For example, if we have a space of 

145 :mod:`~moptipy.spaces.permutations` of the values from `1` to `n`, 

146 where the elements are represented as :class:`numpy.ndarray` objects, 

147 then this function should first check whether `x` is indeed an 

148 instance of :class:`numpy.ndarray`. If not, it should raise a 

149 `TypeError`. Then it could check whether the length of `x` is indeed 

150 `n` and raise a `ValueError` otherwise. It could then check whether 

151 each element of `x` is from `1..n` *and* occurs exactly one time (and 

152 otherwise raise a `ValueError`). Moreover, it should also check 

153 whether the numpy `dtype` of `x` is an appropriate integer data type 

154 and raise a `ValueError` otherwise. Hence, if this function checks an 

155 element `x` and does not raise any error, the user can rely on that 

156 this element `x` is, indeed, a fully valid permutation. 

157 

158 In our system, we use this method for example at the end of 

159 optimization processes. Every solution that is written to a log file 

160 must pass through this method. In other words, we ensure that only 

161 valid solutions are stored. If the optimization algorithm or a search 

162 operator has a bug and sometimes may produce invalid data structures, 

163 this a) helps us in finding the bug and b) prevents us from storing 

164 invalid solutions. It is also strongly encouraged that the 

165 :meth:`from_str` method of any :class:`Space` implementation should 

166 run its results through :meth:`validate`. Since :meth:`from_str` can 

167 be used to, e.g., parse the data from the result sections of log 

168 files, this ensures that no corrupted or otherwise invalid data is 

169 parsed into an application. 

170 See https://thomasweise.github.io/moptipy/#data-formats for more 

171 information on log files. 

172 

173 :param x: the point 

174 :raises TypeError: if the point `x` (or one of its elements, if 

175 applicable) has the wrong data type 

176 :raises ValueError: if the point `x` is invalid and/or simply is not 

177 an element of this space 

178 """ 

179 

180 def n_points(self) -> int: 

181 """ 

182 Get the approximate number of different elements in the space. 

183 

184 This operation can help us when doing tests of the space API 

185 implementations. If we know how many points exist in the space, 

186 then we can judge whether a method that randomly generates 

187 points is sufficiently random, for instance. 

188 

189 By default, this method simply returns `2`. If you have a better 

190 approximation of the size of the space, then you should override it. 

191 

192 :return: the approximate scale of the space 

193 """ 

194 return 2 

195 

196 

197def check_space(space: Any, none_is_ok: bool = False) -> Space | None: 

198 """ 

199 Check whether an object is a valid instance of :class:`Space`. 

200 

201 :param space: the object 

202 :param none_is_ok: is it ok if `None` is passed in? 

203 :return: the object 

204 :raises TypeError: if `space` is not an instance of 

205 :class:`~moptipy.api.space.Space` 

206 

207 >>> check_space(Space()) 

208 Space 

209 >>> check_space(Space(), True) 

210 Space 

211 >>> check_space(Space(), False) 

212 Space 

213 >>> try: 

214 ... check_space('A') 

215 ... except TypeError as te: 

216 ... print(te) 

217 space should be an instance of moptipy.api.space.\ 

218Space but is str, namely 'A'. 

219 >>> try: 

220 ... check_space('A', True) 

221 ... except TypeError as te: 

222 ... print(te) 

223 space should be an instance of moptipy.api.space.\ 

224Space but is str, namely 'A'. 

225 >>> try: 

226 ... check_space('A', False) 

227 ... except TypeError as te: 

228 ... print(te) 

229 space should be an instance of moptipy.api.space.\ 

230Space but is str, namely 'A'. 

231 >>> 

232 >>> try: 

233 ... check_space(None) 

234 ... except TypeError as te: 

235 ... print(te) 

236 space should be an instance of moptipy.api.space.\ 

237Space but is None. 

238 >>> print(check_space(None, True)) 

239 None 

240 >>> try: 

241 ... check_space(None, False) 

242 ... except TypeError as te: 

243 ... print(te) 

244 space should be an instance of moptipy.api.space.\ 

245Space but is None. 

246 """ 

247 if isinstance(space, Space): 

248 return space 

249 if none_is_ok and (space is None): 

250 return None 

251 raise type_error(space, "space", Space)