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 10KB

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