Genetic Turmit Evolver
python
c
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

world.py 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. """ @brief This module contains stuff that represent Turmit's world """
  2. import colorsys
  3. import scipy.misc
  4. import logging
  5. import time
  6. import numpy as np
  7. from random import randint
  8. from .turmit import Turmit
  9. logger = logging.getLogger()
  10. def eval_prog(args):
  11. ''' @brief Return an array of fractdim
  12. @param args : dirty argparser returned arguments
  13. '''
  14. generation, progid, prog, trynum, args = args
  15. w = World(args.world_height, args.world_width, gray=True)
  16. logger.debug('Running P%d run#%d %s' % (progid, trynum, prog))
  17. w.raz()
  18. turmits = [LivingTurmit(world=w, prog=prog, diag=not args.no_diagonals)
  19. for _ in range(args.turmit_count)]
  20. start = time.time()
  21. if not args.mod_steps or trynum == 0:
  22. steps = args.steps
  23. else:
  24. steps = args.steps - int((args.steps / 1.5) // (args.repeat_eval - trynum))
  25. for step in range(steps):
  26. for turmit in turmits:
  27. turmit()
  28. stop = time.time()
  29. score_dir = sum([t.dirvar() for t in turmits]) / len(turmits)
  30. score_fract = w.fractdim()
  31. score = score_dir * score_fract
  32. sinfo = {'F':score_fract, 'D':score_dir}
  33. #score = sum([score_fract] + [score_dir] * 4) / 5
  34. if not args.quiet:
  35. msg = 'G%d P%d run#%d %d steps scores %.3f:(D:%.3f,F:%.3f) in %.2fs (%dus per step)\t%s'
  36. msg %= (generation, progid, trynum, steps, score, score_dir,
  37. score_fract, stop - start,
  38. ((stop - start)*1000000)//steps//len(turmits),
  39. str(prog))
  40. logger.info(msg)
  41. return progid, score, sinfo
  42. class World(object):
  43. ''' @brief A Turmit world. An array of dim == 2 with tuple(R,G,B) '''
  44. def __init__(self, x, y, gray=False):
  45. if x < 1:
  46. raise ValueError('Invalid value %d for x size')
  47. if y < 1:
  48. raise ValueError('Invalid value %d for x size')
  49. self._x = x
  50. self._y = y
  51. self._val = None
  52. self._gray = gray
  53. self.raz()
  54. @property
  55. def height(self):
  56. return self._y
  57. @property
  58. def width(self):
  59. return self._x
  60. def raz(self, color=None):
  61. ''' @brief Set all value to color
  62. @param color tuple | float : tuple(R,G,B) or greylevel
  63. '''
  64. if color is None:
  65. if self._gray:
  66. self._val = np.zeros([self._y, self._x])
  67. else:
  68. self._val = np.zeros([self._y, self._x, 3])
  69. else:
  70. self._val = [[color for _ in range(self._x)] for _ in range(self._y)]
  71. self._val = np.array(self._val)
  72. def save(self, filename):
  73. ''' @brief Save as an image '''
  74. scipy.misc.imsave(filename, self._val)
  75. def fractdim(self, threshold=None):
  76. ''' @return Minkowski–Bouligand dimension (computed) '''
  77. if not self._gray:
  78. Z = self.rgb2gray(self._val)
  79. else:
  80. Z = self._val
  81. # Only for 2d image
  82. assert(len(Z.shape) == 2)
  83. # From https://github.com/rougier/numpy-100 (#87)
  84. def boxcount(Z, k):
  85. S = np.add.reduceat(
  86. np.add.reduceat(Z, np.arange(0, Z.shape[0], k), axis=0),
  87. np.arange(0, Z.shape[1], k), axis=1)
  88. # We count non-empty (0) and non-full boxes (k*k)
  89. return len(np.where((S > 0) & (S < k*k))[0])
  90. if threshold is None:
  91. threshold = np.mean(Z)
  92. if threshold < 0.2:
  93. threshold = 0.2
  94. # Transform Z into a binary array
  95. Z = (Z < threshold)
  96. # Minimal dimension of image
  97. p = min(Z.shape)
  98. # Greatest power of 2 less than or equal to p
  99. n = 2**np.floor(np.log(p)/np.log(2))
  100. # Extract the exponent
  101. n = int(np.log(n)/np.log(2))
  102. # Build successive box sizes (from 2**n down to 2**1)
  103. sizes = 2**np.arange(n, 1, -1)
  104. # Actual box counting with decreasing size
  105. counts = []
  106. for size in sizes:
  107. counts.append(boxcount(Z, size))
  108. # Fit the successive log(sizes) with log (counts)
  109. coeffs = np.polyfit(np.log(sizes), np.log(counts), 1)
  110. return -coeffs[0]
  111. @staticmethod
  112. def rgb2gray(rgb):
  113. r, g, b = rgb[:,:,0], rgb[:,:,1], rgb[:,:,2]
  114. gray = 0.2989 * r + 0.5870 * g + 0.1140 * b
  115. return gray
  116. @staticmethod
  117. def single_rgb2gray(rgb):
  118. r,g,b = rgb
  119. gray = 0.2989 * r + 0.5870 * g + 0.1140 * b
  120. return gray
  121. def __getitem__(self, a):
  122. return self._val[a]
  123. def __setitem__(self, a, b):
  124. raise RuntimeError('No allowed')
  125. class LivingTurmit(Turmit):
  126. ''' @brief Represent a Turmit with coordinates in a world '''
  127. def __init__(self, world, color = None, x=None, y=None, diag=True, **kwargs):
  128. super().__init__(**kwargs)
  129. if not isinstance(world, World):
  130. msg = 'World argument expected to be World but %s found'
  131. raise TypeError(msg % type(world))
  132. if color is not None and len(color) != 3:
  133. msg = 'Color expected to be a tuple(R,G,B) but %s found'
  134. raise TypeError(msg % color)
  135. ## @brief If true direction contains diagonals
  136. self._diag = bool(diag)
  137. ## @brief Stores Turmit color
  138. rand_hue = randint(0,255) / 255
  139. rand_lvl = randint(127,255) / 255
  140. rand_val = randint(127,255) / 255
  141. self._col = tuple(round(i * 255)
  142. for i in colorsys.hsv_to_rgb(rand_hue,
  143. rand_lvl,
  144. rand_val))
  145. if world._gray == 'bw':
  146. self._col = 255.0
  147. elif world._gray:
  148. self._col = tuple(round(i * 255)
  149. for i in colorsys.hsv_to_rgb(rand_hue,1,1))
  150. self._col = World.single_rgb2gray(self._col)
  151. if color is not None:
  152. self._col = color
  153. ## @brief @ref World instance
  154. self._world = world
  155. self._x = randint(0, world.width - 1)
  156. if x is not None:
  157. if x < 0 or x >= world.width:
  158. raise ValueError('Invalid X position %d' % x)
  159. self._x = x
  160. self._y = randint(0, world.height - 1)
  161. if y is not None:
  162. if y < 0 or y >= world.height:
  163. raise ValueError('Invalid Y position %d' % y)
  164. self._y = y
  165. # Direction variation addon
  166. self._absx = self._absy = 0
  167. self._prev_abs = (0, 0)
  168. self._dirvar = 0
  169. self._prev_dirvar = None
  170. self._dirvar_res = 64
  171. self._steps = 1
  172. def __call__(self):
  173. ''' @brief Exec a turmit and move it '''
  174. if self._world._gray:
  175. r = g = b = self._world[self._y][self._x]
  176. else:
  177. r,g,b = self._world[self._y][self._x]
  178. tdir = super().__call__(x=self._x, y=self._y, r=r, g=g, b=b)
  179. self.move(tdir)
  180. # direction variation handling
  181. if self._steps % self._dirvar_res == 0:
  182. dy = self._absy - self._prev_abs[0]
  183. dx = self._absx - self._prev_abs[1]
  184. self._prev_abs = (self._absy, self._absx)
  185. if self._prev_dirvar is not None:
  186. pdvar = self._prev_dirvar
  187. self._dirvar += abs(pdvar[0] - dy) + abs(pdvar[1] - dx)
  188. self._prev_dirvar = (dy, dx)
  189. self._steps += 1
  190. def dirvar(self):
  191. ''' @brief Process @ref _dirvar to return a direction variation
  192. score
  193. @return a score in [0..2]
  194. '''
  195. return self._dirvar / (self._steps * 2)
  196. #d = self._dirvar
  197. #return sum([sum([abs(d[i][j] - d[i+1][j]) for j in (0,1)])
  198. # for i in range(len(d)-1)]) / (self._steps * 2)
  199. def move(self, tdir, setcol=True):
  200. ''' @brief Update coordinates given a direction
  201. @param tdir int : One of 8 directions (integer is understanded
  202. as tdir % 8
  203. @param setcol bool : If True update color of current position
  204. before leaving it
  205. @return y,x new position
  206. '''
  207. if setcol:
  208. self._world[self._y][self._x] = self._col
  209. tdir %= 8 if self._diag else 4
  210. if tdir == 0 or tdir == 4 or tdir == 5:
  211. self._absx += 1
  212. self._x += 1
  213. self._x %= self._world.width
  214. elif tdir == 2 or tdir == 7 or tdir == 6:
  215. self._absx -= 1
  216. if self._x == 0:
  217. self._x = self._world.width -1
  218. else:
  219. self._x -= 1
  220. if tdir == 1 or tdir == 4 or tdir == 6:
  221. self._absy += 1
  222. self._y += 1
  223. self._y %= self._world.height
  224. elif tdir == 3 or tdir == 7 or tdir == 5:
  225. self._absy -= 1
  226. if self._y == 0:
  227. self._y = self._world.height - 1
  228. else:
  229. self._y -= 1
  230. return self._y, self._x