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

32 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +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 >>> try: 

80 ... check_op0(None) 

81 ... except TypeError as te: 

82 ... print(te) 

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

84 """ 

85 if isinstance(op0, Op0): 

86 return op0 

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

88 

89 

90# start op1 

91class Op1(Component): 

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

93 

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

95 """ 

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

97 

98 :param random: the random number generator 

99 :param dest: the destination data structure 

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

101 """ 

102 raise ValueError("Method not implemented!") 

103# end op1 

104 

105 

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

107 """ 

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

109 

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

111 :return: the object 

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

113 

114 >>> check_op1(Op1()) 

115 Op1 

116 >>> try: 

117 ... check_op1('A') 

118 ... except TypeError as te: 

119 ... print(te) 

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

121namely 'A'. 

122 >>> try: 

123 ... check_op1(None) 

124 ... except TypeError as te: 

125 ... print(te) 

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

127 """ 

128 if isinstance(op1, Op1): 

129 return op1 

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

131 

132 

133# start op2 

134class Op2(Component): 

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

136 

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

138 """ 

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

140 

141 :param random: the random number generator 

142 :param dest: the destination data structure 

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

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

145 """ 

146 raise ValueError("Method not implemented!") 

147# end op2 

148 

149 

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

151 """ 

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

153 

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

155 :return: the object `op2` 

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

157 

158 >>> check_op2(Op2()) 

159 Op2 

160 >>> try: 

161 ... check_op2('A') 

162 ... except TypeError as te: 

163 ... print(te) 

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

165namely 'A'. 

166 >>> try: 

167 ... check_op2(None) 

168 ... except TypeError as te: 

169 ... print(te) 

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

171 """ 

172 if isinstance(op2, Op2): 

173 return op2 

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

175 

176 

177# start op1WithStepSize 

178class Op1WithStepSize(Op1): 

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

180 

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

182 """ 

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

184 

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

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

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

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

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

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

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

192 

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

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

195 exact specified size, see, for example module 

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

197 interpret it stochastically as an expectation. Yet others may 

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

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

200 best effort fails, see for example module 

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

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

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

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

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

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

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

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

209 black-box setting. 

210 

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

212 reasonable default value for this parameter ensure compatibility with 

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

214 

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

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

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

218 

219 :param random: the random number generator 

220 :param dest: the destination data structure 

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

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

223 """ 

224 raise ValueError("Method not implemented!") 

225# end op1WithStepSize 

226 

227 

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

229 """ 

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

231 

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

233 :return: the object `op1` 

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

235 

236 >>> check_op1_with_step_size(Op1WithStepSize()) 

237 Op1WithStepSize 

238 >>> try: 

239 ... check_op1_with_step_size('A') 

240 ... except TypeError as te: 

241 ... print(te) 

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

243but is str, namely 'A'. 

244 >>> try: 

245 ... check_op1_with_step_size(Op1()) 

246 ... except TypeError as te: 

247 ... print(te) 

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

249but is moptipy.api.operators.Op1. 

250 >>> try: 

251 ... check_op1_with_step_size(None) 

252 ... except TypeError as te: 

253 ... print(te) 

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

255but is None. 

256 """ 

257 if isinstance(op1, Op1WithStepSize): 

258 return op1 

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