Coverage for moptipy / api / operators.py: 88%

32 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-15 11:18 +0000

1""" 

2The base classes for implementing search operators. 

3 

4Nullary search operators are used to sample the initial starting points of the 

5optimization processes. They inherit from class 

6:class:`~moptipy.api.operators.Op0`. The pre-defined unit test routine 

7:func:`~moptipy.tests.op0.validate_op0` can and should be used to test all the 

8nullary operators that are implemented. 

9 

10Unary search operators accept one point in the search space as input and 

11generate a new, similar point as output. They inherit from class 

12:class:`~moptipy.api.operators.Op1`. The pre-defined unit test routine 

13:func:`~moptipy.tests.op1.validate_op1` can and should be used to test all the 

14unary operators that are implemented. 

15 

16The basic unary operators :class:`~moptipy.api.operators.Op1` have no 

17parameter telling them how much of the input point to change. They may do a 

18hard-coded number of modifications (as, e.g., 

19:class:`~moptipy.operators.permutations.op1_swap2.Op1Swap2` does) or may 

20apply a random number of modifications (like 

21:class:`~moptipy.operators.permutations.op1_swapn.Op1SwapN`). There is a 

22sub-class of unary operators named 

23:class:`~moptipy.api.operators.Op1WithStepSize` where a parameter `step_size` 

24with a value from the closed interval `[0.0, 1.0]` can be supplied. If 

25`step_size=0.0`, such an operator should perform the smallest possible 

26modification and for `step_size=1.0`, it should perform the largest possible 

27modification. 

28 

29Binary search operators accept two points in the search space as input and 

30generate a new point that should be similar to both inputs as output. They 

31inherit from class :class:`~moptipy.api.operators.Op2`. The pre-defined unit 

32test routine :func:`~moptipy.tests.op2.validate_op2` can and should be used to 

33test all the binary operators that are implemented. 

34""" 

35from typing import Any 

36 

37from numpy.random import Generator 

38from pycommons.types import type_error 

39 

40from moptipy.api.component import Component 

41 

42 

43# start op0 

44class Op0(Component): 

45 """A base class to implement a nullary search operator.""" 

46 

47 def op0(self, random: Generator, dest) -> None: 

48 """ 

49 Apply the nullary search operator to fill object `dest`. 

50 

51 Afterwards `dest` will hold a valid point in the search space. 

52 Often, this would be a point uniformly randomly sampled from the 

53 search space, but it could also be the result of a heuristic or 

54 even a specific solution. 

55 

56 :param random: the random number generator 

57 :param dest: the destination data structure 

58 """ 

59 raise ValueError("Method not implemented!") 

60# end op0 

61 

62 

63def check_op0(op0: Any) -> Op0: 

64 """ 

65 Check whether an object is a valid instance of :class:`Op0`. 

66 

67 :param op0: the (supposed) instance of :class:`Op0` 

68 :return: the object `op0` 

69 :raises TypeError: if `op0` is not an instance of :class:`Op0` 

70 

71 >>> check_op0(Op0()) 

72 Op0 

73 >>> try: 

74 ... check_op0('A') 

75 ... except TypeError as te: 

76 ... print(te) 

77 op0 should be an instance of moptipy.api.operators.Op0 but is \ 

78str, namely 'A'. 

79 

80 >>> try: 

81 ... check_op0(None) 

82 ... except TypeError as te: 

83 ... print(te) 

84 op0 should be an instance of moptipy.api.operators.Op0 but is None. 

85 """ 

86 if isinstance(op0, Op0): 

87 return op0 

88 raise type_error(op0, "op0", Op0) 

89 

90 

91# start op1 

92class Op1(Component): 

93 """A base class to implement a unary search operator.""" 

94 

95 def op1(self, random: Generator, dest, x) -> None: 

96 """ 

97 Fill `dest` with a modified copy of `x`. 

98 

99 :param random: the random number generator 

100 :param dest: the destination data structure 

101 :param x: the source point in the search space 

102 """ 

103 raise ValueError("Method not implemented!") 

104# end op1 

105 

106 

107def check_op1(op1: Any) -> Op1: 

108 """ 

109 Check whether an object is a valid instance of :class:`Op1`. 

110 

111 :param op1: the (supposed) instance of :class:`Op1` 

112 :return: the object 

113 :raises TypeError: if `op1` is not an instance of :class:`Op1` 

114 

115 >>> check_op1(Op1()) 

116 Op1 

117 >>> try: 

118 ... check_op1('A') 

119 ... except TypeError as te: 

120 ... print(te) 

121 op1 should be an instance of moptipy.api.operators.Op1 but is str, \ 

122namely 'A'. 

123 

124 >>> try: 

125 ... check_op1(None) 

126 ... except TypeError as te: 

127 ... print(te) 

128 op1 should be an instance of moptipy.api.operators.Op1 but is None. 

129 """ 

130 if isinstance(op1, Op1): 

131 return op1 

132 raise type_error(op1, "op1", Op1) 

133 

134 

135# start op2 

136class Op2(Component): 

137 """A base class to implement a binary search operator.""" 

138 

139 def op2(self, random: Generator, dest, x0, x1) -> None: 

140 """ 

141 Fill `dest` with a combination of `x0` and `x1`. 

142 

143 :param random: the random number generator 

144 :param dest: the destination data structure 

145 :param x0: the first source point in the search space 

146 :param x1: the second source point in the search space 

147 """ 

148 raise ValueError("Method not implemented!") 

149# end op2 

150 

151 

152def check_op2(op2: Any) -> Op2: 

153 """ 

154 Check whether an object is a valid instance of :class:`Op2`. 

155 

156 :param op2: the (supposed) instance of :class:`Op2` 

157 :return: the object `op2` 

158 :raises TypeError: if `op2` is not an instance of :class:`Op2` 

159 

160 >>> check_op2(Op2()) 

161 Op2 

162 >>> try: 

163 ... check_op2('A') 

164 ... except TypeError as te: 

165 ... print(te) 

166 op2 should be an instance of moptipy.api.operators.Op2 but is str, \ 

167namely 'A'. 

168 

169 >>> try: 

170 ... check_op2(None) 

171 ... except TypeError as te: 

172 ... print(te) 

173 op2 should be an instance of moptipy.api.operators.Op2 but is None. 

174 """ 

175 if isinstance(op2, Op2): 

176 return op2 

177 raise type_error(op2, "op2", Op2) 

178 

179 

180# start op1WithStepSize 

181class Op1WithStepSize(Op1): 

182 """A unary search operator with a step size.""" 

183 

184 def op1(self, random: Generator, dest, x, step_size: float = 0.0) -> None: 

185 """ 

186 Copy `x` to `dest` but apply a modification with a given `step_size`. 

187 

188 This operator is similar to :meth:`Op1.op1` in that it stores a 

189 modified copy of `x` into `dest`. The difference is that you can also 

190 specify how much that copy should be different: The parameter 

191 `step_size` can take on any value in the interval `[0.0, 1.0]`, 

192 including the two boundary values. A `step_size` of `0.0` indicates 

193 the smallest possible move (for which `dest` will still be different 

194 from `x`) and `step_size=1.0` will lead to the largest possible move. 

195 

196 The `step_size` may be interpreted differently by different operators: 

197 Some may interpret it as an exact requirement and enforce steps of the 

198 exact specified size, see, for example module 

199 :mod:`~moptipy.operators.bitstrings.op1_flip_m`. Others might 

200 interpret it stochastically as an expectation. Yet others may 

201 interpret it as a goal step width and try to realize it in a best 

202 effort kind of way, but may also do smaller or larger steps if the 

203 best effort fails, see for example module 

204 :mod:`~moptipy.operators.permutations.op1_swap_exactly_n`. 

205 What all operators should, however, have in common is that at 

206 `step_size=0.0`, they should try to perform a smallest possible change 

207 and at `step_size=1.0`, they should try to perform a largest possible 

208 change. For all values in between, step sizes should grow with rising 

209 `step_size`. This should allow algorithms that know nothing about the 

210 nature of the search space or the operator's moves to still tune 

211 between small and large moves based on a policy which makes sense in a 

212 black-box setting. 

213 

214 Every implementation of :class:`Op1WithStepSize` must specify a 

215 reasonable default value for this parameter ensure compatibility with 

216 :meth:`Op1.op1`. In this base class, we set the default to `0.0`. 

217 

218 Finally, if a `step_size` value is passed in which is outside the 

219 interval `[0, 1]`, the behavior of this method is undefined. It may 

220 throw an exception or not. It may also enter an infinite loop. 

221 

222 :param random: the random number generator 

223 :param dest: the destination data structure 

224 :param x: the source point in the search space 

225 :param step_size: the step size parameter for the unary operator 

226 """ 

227 raise ValueError("Method not implemented!") 

228# end op1WithStepSize 

229 

230 

231def check_op1_with_step_size(op1: Any) -> Op1WithStepSize: 

232 """ 

233 Check whether an object is a valid instance of :class:`Op1WithStepSize`. 

234 

235 :param op1: the (supposed) instance of :class:`Op1WithStepSize` 

236 :return: the object `op1` 

237 :raises TypeError: if `op1` is not an instance of :class:`Op1WithStepSize` 

238 

239 >>> check_op1_with_step_size(Op1WithStepSize()) 

240 Op1WithStepSize 

241 >>> try: 

242 ... check_op1_with_step_size('A') 

243 ... except TypeError as te: 

244 ... print(te) 

245 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \ 

246but is str, namely 'A'. 

247 

248 >>> try: 

249 ... check_op1_with_step_size(Op1()) 

250 ... except TypeError as te: 

251 ... print(te) 

252 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \ 

253but is moptipy.api.operators.Op1. 

254 

255 >>> try: 

256 ... check_op1_with_step_size(None) 

257 ... except TypeError as te: 

258 ... print(te) 

259 op1 should be an instance of moptipy.api.operators.Op1WithStepSize \ 

260but is None. 

261 """ 

262 if isinstance(op1, Op1WithStepSize): 

263 return op1 

264 raise type_error(op1, "op1", Op1WithStepSize)