base.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. """Basic Algorithm class
  2. """
  3. # main imports
  4. import logging
  5. import sys, os
  6. from ..utils.progress import macop_text, macop_line, macop_progress
  7. # Generic algorithm class
  8. class Algorithm():
  9. """Abstract Algorithm class used as basic algorithm implementation with some specific initialization
  10. This class enables to manage some common usages of operation research algorithms:
  11. - initialization function of solution
  12. - validator function to check if solution is valid or not (based on some criteria)
  13. - evaluation function to give fitness score to a solution
  14. - operators used in order to update solution during search process
  15. - policy process applied when choosing next operator to apply
  16. - callbacks function in order to do some relative stuff every number of evaluation or reload algorithm state
  17. - parent algorithm associated to this new algorithm instance (hierarchy management)
  18. Attributes:
  19. initializer: {function} -- basic function strategy to initialize solution
  20. evaluator: {function} -- basic function in order to obtained fitness (mono or multiple objectives)
  21. operators: {[Operator]} -- list of operator to use when launching algorithm
  22. policy: {Policy} -- Policy class implementation strategy to select operators
  23. validator: {function} -- basic function to check if solution is valid or not under some constraints
  24. maximise: {bool} -- specify kind of optimisation problem
  25. verbose: {bool} -- verbose or not information about the algorithm
  26. currentSolution: {Solution} -- current solution managed for current evaluation comparison
  27. bestSolution: {Solution} -- best solution found so far during running algorithm
  28. callbacks: {[Callback]} -- list of Callback class implementation to do some instructions every number of evaluations and `load` when initializing algorithm
  29. parent: {Algorithm} -- parent algorithm reference in case of inner Algorithm instance (optional)
  30. """
  31. def __init__(self,
  32. initializer,
  33. evaluator,
  34. operators,
  35. policy,
  36. validator,
  37. maximise=True,
  38. parent=None,
  39. verbose=True):
  40. # protected members intialization
  41. self._initializer = initializer
  42. self._evaluator = evaluator
  43. self._operators = operators
  44. self._policy = policy
  45. self._validator = validator
  46. self._callbacks = []
  47. self._bestSolution = None
  48. self._currentSolution = None
  49. # by default
  50. self._numberOfEvaluations = 0
  51. self._maxEvaluations = 0
  52. # other parameters
  53. self._parent = parent # parent algorithm if it's sub algorithm
  54. #self.maxEvaluations = 0 # by default
  55. self._maximise = maximise
  56. self._verbose = verbose
  57. # track reference of algorihtm into operator (keep an eye into best solution)
  58. for operator in self._operators:
  59. if self._parent is not None:
  60. operator.setAlgo(self.getParent())
  61. else:
  62. operator.setAlgo(self)
  63. # also track reference for policy
  64. if self._parent is not None:
  65. self._policy.setAlgo(self.getParent())
  66. else:
  67. self._policy.setAlgo(self)
  68. def addCallback(self, _callback):
  69. """Add new callback to algorithm specifying usefull parameters
  70. Args:
  71. _callback: {Callback} -- specific Callback instance
  72. """
  73. # specify current main algorithm reference for callback
  74. if self._parent is not None:
  75. _callback.setAlgo(self.getParent())
  76. else:
  77. _callback.setAlgo(self)
  78. # set as new
  79. self._callbacks.append(_callback)
  80. def resume(self):
  81. """Resume algorithm using Callback instances
  82. """
  83. # load every callback if many things are necessary to do before running algorithm
  84. for callback in self._callbacks:
  85. callback.load()
  86. def getParent(self):
  87. """Recursively find the main parent algorithm attached of the current algorithm
  88. Returns:
  89. {Algorithm} -- main algorithm set for this algorithm
  90. """
  91. current_algorithm = self
  92. parent_alrogithm = None
  93. # recursively find the main algorithm parent
  94. while current_algorithm._parent is not None:
  95. parent_alrogithm = current_algorithm._parent
  96. current_algorithm = current_algorithm._parent
  97. return parent_alrogithm
  98. def initRun(self):
  99. """
  100. Initialize the current solution and best solution using the `initialiser` function
  101. """
  102. self._currentSolution = self._initializer()
  103. # evaluate current solution
  104. self._currentSolution.evaluate(self._evaluator)
  105. self.increaseEvaluation()
  106. # keep in memory best known solution (current solution)
  107. if self._bestSolution is None:
  108. self._bestSolution = self._currentSolution
  109. def increaseEvaluation(self):
  110. """
  111. Increase number of evaluation once a solution is evaluated for each dependant algorithm (parents hierarchy)
  112. """
  113. current_algorithm = self
  114. while current_algorithm is not None:
  115. current_algorithm._numberOfEvaluations += 1
  116. current_algorithm = current_algorithm._parent
  117. def getGlobalEvaluation(self):
  118. """Get the global number of evaluation (if inner algorithm)
  119. Returns:
  120. {int} -- current global number of evaluation
  121. """
  122. parent_algorithm = self.getParent()
  123. if parent_algorithm is not None:
  124. return parent_algorithm.getGlobalEvaluation()
  125. return self._numberOfEvaluations
  126. def getGlobalMaxEvaluation(self):
  127. """Get the global max number of evaluation (if inner algorithm)
  128. Returns:
  129. {int} -- current global max number of evaluation
  130. """
  131. parent_algorithm = self.getParent()
  132. if parent_algorithm is not None:
  133. return parent_algorithm.getGlobalMaxEvaluation()
  134. return self._maxEvaluations
  135. def stop(self):
  136. """
  137. Global stopping criteria (check for parents algorithm hierarchy too)
  138. """
  139. parent_algorithm = self.getParent()
  140. # based on global stopping creteria or on its own stopping critera
  141. if parent_algorithm is not None:
  142. return parent_algorithm._numberOfEvaluations >= parent_algorithm._maxEvaluations or self._numberOfEvaluations >= self._maxEvaluations
  143. return self._numberOfEvaluations >= self._maxEvaluations
  144. def evaluate(self, solution):
  145. """
  146. Evaluate a solution using evaluator passed when intialize algorithm
  147. Args:
  148. solution: {Solution} -- solution to evaluate
  149. Returns:
  150. {float} -- fitness score of solution which is not already evaluated or changed
  151. Note:
  152. if multi-objective problem this method can be updated using array of `evaluator`
  153. """
  154. return solution.evaluate(self._evaluator)
  155. def update(self, solution):
  156. """
  157. Apply update function to solution using specific `policy`
  158. Check if solution is valid after modification and returns it
  159. Args:
  160. solution: {Solution} -- solution to update using current policy
  161. Returns:
  162. {Solution} -- updated solution obtained by the selected operator
  163. """
  164. # two parameters are sent if specific crossover solution are wished
  165. sol = self._policy.apply(solution)
  166. # compute fitness of new solution if not already computed
  167. if sol._score is None:
  168. sol.evaluate(self._evaluator)
  169. if (sol.isValid(self._validator)):
  170. return sol
  171. else:
  172. logging.info("-- New solution is not valid %s" % sol)
  173. return solution
  174. def isBetter(self, solution):
  175. """
  176. Check if solution is better than best found
  177. - if the new solution is not valid then the fitness comparison is not done
  178. - fitness comparison is done using problem nature (maximising or minimising)
  179. Args:
  180. solution: {Solution} -- solution to compare with best one
  181. Returns:
  182. {bool} -- `True` if better
  183. """
  184. if not solution.isValid(self._validator):
  185. return False
  186. # depending of problem to solve (maximizing or minimizing)
  187. if self._maximise:
  188. if solution.fitness() > self._bestSolution.fitness():
  189. return True
  190. else:
  191. if solution.fitness() < self._bestSolution.fitness():
  192. return True
  193. # by default
  194. return False
  195. def run(self, evaluations):
  196. """
  197. Run the specific algorithm following number of evaluations to find optima
  198. """
  199. # append number of max evaluation if multiple run called
  200. self._maxEvaluations += evaluations
  201. # check if global evaluation is used or not
  202. if self.getParent() is not None and self.getGlobalEvaluation() != 0:
  203. # init number evaluations of inner algorithm depending of globalEvaluation
  204. # allows to restart from `checkpoint` last evaluation into inner algorithm
  205. rest = self.getGlobalEvaluation() % self._maxEvaluations
  206. self._numberOfEvaluations = rest
  207. else:
  208. self._numberOfEvaluations = 0
  209. logging.info("Run %s with %s evaluations" %
  210. (self.__str__(), evaluations))
  211. def progress(self):
  212. """
  213. Log progress and apply callbacks if necessary
  214. """
  215. if len(self._callbacks) > 0:
  216. for callback in self._callbacks:
  217. callback.run()
  218. if self._verbose:
  219. macop_progress(self, self.getGlobalEvaluation(), self.getGlobalMaxEvaluation())
  220. logging.info(f"-- {type(self).__name__} evaluation {self._numberOfEvaluations} of {self._maxEvaluations} ({((self._numberOfEvaluations / self._maxEvaluations) * 100):.2f}%) - BEST SCORE {self._bestSolution.fitness()}")
  221. def end(self):
  222. """Display end message into `run` method
  223. """
  224. macop_text(self, f'({type(self).__name__}) Found after {self._numberOfEvaluations} evaluations \n - {self._bestSolution}')
  225. macop_line(self)
  226. def information(self):
  227. logging.info(f"-- Best {self._bestSolution} - SCORE {self._bestSolution.fitness()}")
  228. # def __blockPrint(self):
  229. # """Private method which disables console prints when running algorithm if specified when instancing algorithm
  230. # """
  231. # sys.stdout = open(os.devnull, 'w')
  232. # def __enablePrint(self):
  233. # """Private which enables console prints when running algorithm
  234. # """
  235. # sys.stdout = sys.__stdout__
  236. # def __del__(self):
  237. # # enable prints when object is deleted
  238. # if not self._verbose:
  239. # self.__enablePrint()
  240. def __str__(self):
  241. return f"{type(self).__name__} using {type(self._bestSolution).__name__}"