Coverage for moptipy / algorithms / so / ma.py: 98%

84 statements  

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

1""" 

2A simple implementation of a (mu+lambda) Memetic Algorithm. 

3 

4A memetic algorithm (:class:`~moptipy.algorithms.so.ma.MA`) works similar to a 

5(mu+lambda) :class:`~moptipy.algorithms.so.ea.EA`, but it refines all results 

6of the search operators with a local search 

7:attr:`~moptipy.algorithms.so.ma.MA.ls` executed for 

8:attr:`~moptipy.algorithms.so.ma.MA.ls_fes` objective function evaluations. 

9It also only employs the nullary search operator (to create the initial random 

10solutions) and the binary search operator (to combine two selected parental 

11solutions). 

12 

13This is the general form of the Memetic Algorithm of the type 

14"EA+something else." A specialized version combining an 

15:mod:`~moptipy.algorithms.so.ea` with 

16:mod:`~moptipy.algorithms.so.rls` can be found in 

17:mod:`~moptipy.algorithms.so.marls`. 

18 

19This Memetic Algorithm implementation begins by sampling 

20:attr:`~moptipy.algorithms.so.ma.MA.mu` 

21solutions using the nullary search operation 

22:attr:`~moptipy.api.algorithm.Algorithm0.op0`. Each of these initial solutions 

23is used as a starting point of a local search 

24:attr:`~moptipy.algorithms.so.ma.MA.ls`, which is executed for 

25:attr:`~moptipy.algorithms.so.ma.MA.ls_fes` objective function evaluations. 

26In each iteration, it then uses the 

27:attr:`~moptipy.algorithms.so.ma.MA.mu` existing solutions as input for 

28the binary search operator :attr:`~moptipy.algorithms.so.ma.MA.op2` to create 

29:attr:`~moptipy.algorithms.so.ma.MA.lambda_` new solutions, each of which is 

30again used as a starting point of a local search 

31:attr:`~moptipy.algorithms.so.ma.MA.ls` executed for 

32:attr:`~moptipy.algorithms.so.ma.MA.ls_fes` objective function evaluations. 

33The results of the local searches enter the population and in the next 

34iteration, the :attr:`~moptipy.algorithms.so.ma.MA.mu` best solutions of the 

35:attr:`~moptipy.algorithms.so.ma.MA.mu` + 

36:attr:`~moptipy.algorithms.so.ma.MA.lambda_` ones in the population are 

37retained. 

38 

39Due to the :class:`~moptipy.api.process.Process` and 

40:mod:`~moptipy.api.subprocesses` API of `moptipy`, you can use almost arbitrary 

41algorithms as local search :attr:`~moptipy.algorithms.so.ma.MA.ls`. The only 

42requirement is that is a subclass of 

43:class:`~moptipy.api.algorithm.Algorithm0` and uses 

44it uses an instance of 

45:class:`moptipy.operators.op0_forward.Op0Forward` as nullary search operator 

46(:attr:`~moptipy.api.algorithm.Algorithm0.op0`). 

47This allows the MA to set a solution as starting point for the inner algorithm 

48:attr:`~moptipy.algorithms.so.ma.MA.ls`. 

49 

50It should be noted that it is by no means required that the inner algorithm 

51:attr:`~moptipy.algorithms.so.ma.MA.ls` needs to be a local search. Any 

52algorithm that fulfills the above requirements could be used. For example, if 

53we were conducting numerical optimization, it would be totally OK to plug an 

54instance of the Sequential Least Squares Programming 

55(:class:`~moptipy.algorithms.so.vector.scipy.SLSQP`) algorithm into the 

56memetic algorithm… 

57 

58Further reading on how to realize using one algorithm as a sub-algorithm 

59of another one can be found in the documentation of 

60:func:`~moptipy.api.subprocesses.from_starting_point`, 

61:func:`~moptipy.api.subprocesses.for_fes`, and 

62:class:`moptipy.operators.op0_forward.Op0Forward`. 

63 

641. Pablo Moscato. *On Evolution, Search, Optimization, Genetic Algorithms and 

65 Martial Arts: Towards Memetic Algorithms.* Caltech Concurrent Computation 

66 Program Report C3P 826. 1989. Pasadena, CA, USA: California Institute of 

67 Technology (Caltech), Caltech Concurrent Computation Program (C3P). 

68 https://www.researchgate.net/publication/2354457 

692. Carlos Cotta, Luke Mathieson, and Pablo Moscato. Memetic Algorithms. In 

70 Rafael Martí, Panos M. Pardalos, and Mauricio G. C. Resende, editors, 

71 *Handbook of Heuristics.* Part~III: Metaheuristics, pages 607-638. 2018. 

72 Cham, Switzerland: Springer. ISBN: 978-3-319-07123-7. 

73 doi: https://doi.org/10.1007/978-3-319-07153-4_29-1 

74 https://www.researchgate.net/publication/315660932 

753. William Eugene Hart, James E. Smith, and Natalio Krasnogor, editors. 

76 *Recent Advances in Memetic Algorithms.* Studies in Fuzziness and Soft 

77 Computing (STUDFUZZ), volume 166. 2005. Berlin/Heidelberg, Germany: 

78 Springer. ISBN: 978-3-540-22904-9. 

79 doi: https://doi.org/10.1007/3-540-32363-5 

804. Ferrante Neri, Carlos Cotta, and Pablo Moscato. *Handbook of Memetic 

81 Algorithms.* Volume 379 of Studies in Computational Intelligence (SCI). 

82 2012. Berlin/Heidelberg, Germany: Springer. ISBN: 978-3-642-23246-6 

83 doi https://doi.org/10.1007/978-3-642-23247-3. 

845. L. Darrell Whitley, V. Scott Gordon, and Keith E. Mathias. Lamarckian 

85 Evolution, The Baldwin Effect and Function Optimization. In Yuval Davidor, 

86 Hans-Paul Schwefel, and Reinhard Männer, editors, *Proceedings of the Third 

87 Conference on Parallel Problem Solving from Nature; International 

88 Conference on Evolutionary Computation (PPSN III),* October 9-14, 1994, 

89 Jerusalem, Israel, pages 5-15. Volume 866/1994 of Lecture Notes in Computer 

90 Science, Berlin, Germany: Springer-Verlag GmbH. ISBN 0387584846. 

91 doi: https://doi.org/10.1007/3-540-58484-6_245. 

92 https://www.researchgate.net/publication/2521727 

93""" 

94from typing import Callable, Final, cast 

95 

96from numpy.random import Generator 

97from pycommons.types import check_int_range, type_error 

98 

99from moptipy.algorithms.so.record import Record 

100from moptipy.api.algorithm import Algorithm0 

101from moptipy.api.logging import SCOPE_OP2 

102from moptipy.api.operators import Op0, Op2, check_op2 

103from moptipy.api.process import Process 

104from moptipy.api.subprocesses import for_fes, from_starting_point 

105from moptipy.operators.op0_forward import Op0Forward 

106from moptipy.utils.logger import KeyValueLogSection 

107from moptipy.utils.strings import PART_SEPARATOR 

108 

109 

110# start book 

111class MA(Algorithm0): 

112 """An MA is a population-based algorithm using binary operators.""" 

113 

114 def solve(self, process: Process) -> None: 

115 """ 

116 Apply the MA to an optimization problem. 

117 

118 :param process: the black-box process object 

119 """ 

120 # initialization of some variables omitted in book for brevity 

121 # end book 

122 mu: Final[int] = self.mu # mu: number of best solutions kept 

123 mu_plus_lambda: Final[int] = mu + self.lambda_ # size 

124 random: Final[Generator] = process.get_random() # random gen 

125 create: Final[Callable] = process.create # create x container 

126 evaluate: Final[Callable] = process.evaluate # the objective 

127 op0: Final[Callable] = self.op0.op0 # the nullary operator 

128 op2: Final[Callable] = self.op2.op2 # the binary operator 

129 ls_fes: Final[int] = self.ls_fes # the number of FEs per ls run 

130 ls_solve: Final[Callable[[Process], None]] = self.ls.solve # +book 

131 forward_ls_op0_to: Final[Callable] = cast( # forward starting 

132 "Op0Forward", self.ls.op0).forward_to # point of ls to... 

133 should_terminate: Final[Callable] = process.should_terminate 

134 r0i: Final[Callable[[int], int]] = cast( # random integers 

135 "Callable[[int], int]", random.integers) 

136 # start book 

137 # create list of mu random+ls records and lambda empty records 

138 lst: Final[list] = [None] * mu_plus_lambda # pre-allocate list 

139 f: int | float = 0 # variable to hold objective values 

140 for i in range(mu_plus_lambda): # fill list of size mu+lambda 

141 x = create() # by creating point in search space 

142 if i < mu: # only the first mu records are initialized by 

143 op0(random, x) # applying nullary operator = randomize 

144 if should_terminate(): # should we stop now? 

145 cast("Op0Forward", self.ls.op0).stop_forwarding() # -book 

146 return # computational budget exhausted -> quit 

147 with for_fes(process, ls_fes) as s1, \ 

148 from_starting_point(s1, x, evaluate(x)) as s2: 

149 forward_ls_op0_to(s2.get_copy_of_best_x) 

150 ls_solve(s2) # apply local search modifying x 

151 f = s2.get_best_f() # get quality of x 

152 lst[i] = Record(x, f) # create and store record 

153 

154 it: int = 0 # set iteration counter=0 (but immediately increment) 

155 while True: # lst: keep 0..mu-1, overwrite mu..mu+lambda-1 

156 it += 1 # step iteration counter 

157 for oi in range(mu, mu_plus_lambda): # for all offspring 

158 if should_terminate(): # should we stop now? 

159 cast("Op0Forward", self.ls.op0).stop_forwarding() # -book 

160 return # computational budget exhausted -> quit 

161 dest: Record = lst[oi] # pick destination record 

162 x = dest.x # the destination "x" value 

163 dest.it = it # remember iteration of solution creation 

164 

165 sx = lst[r0i(mu)].x # pick random first source "x" 

166 sx2 = sx # second source "x" initially=first sx 

167 while sx2 is sx: # until different from sx... 

168 sx2 = lst[r0i(mu)].x # ..get random second "x" 

169 op2(random, x, sx, sx2) # apply binary operator 

170 with for_fes(process, ls_fes) as s1, \ 

171 from_starting_point(s1, x, evaluate(x)) as s2: 

172 forward_ls_op0_to(s2.get_copy_of_best_x) 

173 ls_solve(s2) # apply local search modifying x 

174 dest.f = s2.get_best_f() # get quality of x 

175 lst.sort() # best records come first, ties broken by age 

176 # end book 

177 

178 def __init__(self, op0: Op0, 

179 op2: Op2, ls: Algorithm0, 

180 mu: int = 2, lambda_: int = 1, 

181 ls_fes: int = 1000, 

182 name: str = "ma") -> None: 

183 """ 

184 Create the Memetic Algorithm (MA). 

185 

186 :param op0: the nullary search operator 

187 :param op2: the binary search operator 

188 :param ls: the local search to apply to each new solution 

189 :param mu: the number of best solutions to survive in each generation 

190 :param lambda_: the number of offspring in each generation 

191 :param ls_fes: the number of FEs (steps) per local search run 

192 :param name: the base name of the algorithm 

193 """ 

194 super().__init__(f"{name}{PART_SEPARATOR}{mu}{PART_SEPARATOR}" 

195 f"{lambda_}{PART_SEPARATOR}{ls_fes}{PART_SEPARATOR}" 

196 f"{op2}{PART_SEPARATOR}{ls}", op0) 

197 if not isinstance(ls, Algorithm0): 

198 raise type_error(ls, "ls", Algorithm0) 

199 if not isinstance(ls.op0, Op0Forward): 

200 raise type_error(ls.op0, "ls.op0", Op0Forward) 

201 #: the number of records to survive in each generation 

202 self.mu: Final[int] = check_int_range(mu, "mu", 1, 1_000_000) 

203 #: the number of offsprings per generation 

204 self.lambda_: Final[int] = check_int_range( 

205 lambda_, "lambda", 1, 1_000_000) 

206 #: the number of FEs per local search run 

207 self.ls_fes: Final[int] = check_int_range( 

208 ls_fes, "ls_fes", 1, 100_000_000) 

209 #: The binary search operator. 

210 self.op2: Final[Op2] = check_op2(op2) 

211 #: the local search algorithm 

212 self.ls: Final[Algorithm0] = ls 

213 

214 def log_parameters_to(self, logger: KeyValueLogSection) -> None: 

215 """ 

216 Log the parameters of the algorithm to a logger. 

217 

218 :param logger: the logger for the parameters 

219 """ 

220 super().log_parameters_to(logger) 

221 logger.key_value("mu", self.mu) 

222 logger.key_value("lambda", self.lambda_) 

223 logger.key_value("lsFEs", self.ls_fes) 

224 with logger.scope(SCOPE_OP2) as o: 

225 self.op2.log_parameters_to(o) 

226 with logger.scope("ls") as ls: 

227 self.ls.log_parameters_to(ls) 

228 

229 def initialize(self) -> None: 

230 """Initialize the memetic algorithm.""" 

231 super().initialize() 

232 self.ls.initialize() 

233 self.op2.initialize()