population.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. """
  2. pygene/population.py - Represents a population of organisms
  3. """
  4. import random
  5. from random import randrange, choice
  6. from math import sqrt
  7. from organism import Organism, BaseOrganism
  8. from xmlio import PGXmlMixin
  9. class Population(PGXmlMixin):
  10. """
  11. Represents a population of organisms
  12. You might want to subclass this
  13. Overridable class variables:
  14. - species - Organism class or subclass, being the 'species'
  15. of organism comprising this population
  16. - initPopulation - size of population to randomly create
  17. if no organisms are passed in to constructor
  18. - childCull - cull to this many children after each generation
  19. - childCount - number of children to create after each generation
  20. - incest - max number of best parents to mix amongst the
  21. kids for next generation, default 10
  22. - numNewOrganisms - number of random new orgs to add each
  23. generation, default 0
  24. - initPopulation - initial population size, default 10
  25. - mutants - default 0.1 - if mutateAfterMating is False,
  26. then this sets the percentage of mutated versions of
  27. children to add to the child population; children to mutate
  28. are selected based on fitness
  29. Supports the following python operators:
  30. - + - produces a new population instances, whose members are
  31. an aggregate of the members of the values being added
  32. - [] - int subscript - returns the ith fittest member
  33. """
  34. # cull to this many children after each generation
  35. childCull = 20
  36. # number of children to create after each generation
  37. childCount = 10
  38. # max number of best parents to mix amongst the kids for
  39. # next generation
  40. incest = 5
  41. # parameters governing addition of random new organisms
  42. numNewOrganisms = 0 # number of new orgs to add each generation
  43. # set to initial population size
  44. initPopulation = 10
  45. # set to species of organism
  46. species = Organism
  47. # mutate this proportion of organisms
  48. mutants = 0.2
  49. # set this to true to mutate all progeny
  50. mutateAfterMating = True
  51. def __init__(self, *items, **kw):
  52. """
  53. Create a population with zero or more members
  54. Arguments:
  55. - any number of arguments and/or sequences of args,
  56. where each arg is an instance of this population's
  57. species. If no arguments are given, organisms are
  58. randomly created and added automatically, according
  59. to self.initPopulation and self.species
  60. Keywords:
  61. - init - size of initial population to randomly create.
  62. Ignored if 1 or more constructor arguments are given.
  63. if not given, value comes from self.initPopulation
  64. - species - species of organism to create and add. If not
  65. given, value comes from self.species
  66. """
  67. self.organisms = []
  68. if kw.has_key('species'):
  69. species = self.species = kw['species']
  70. else:
  71. species = self.species
  72. if kw.has_key('init'):
  73. init = self.initPopulation = kw['init']
  74. else:
  75. init = self.initPopulation
  76. if not items:
  77. for i in xrange(init):
  78. self.add(species())
  79. else:
  80. self.add(*items)
  81. def add(self, *args):
  82. """
  83. Add an organism, or a population of organisms,
  84. to this population
  85. You can also pass lists or tuples of organisms and/or
  86. populations, to any level of nesting
  87. """
  88. for arg in args:
  89. if isinstance(arg, tuple) or isinstance(arg, list):
  90. # got a list of things, add them one by one
  91. self.add(*arg)
  92. if isinstance(arg, BaseOrganism):
  93. # add single organism
  94. self.organisms.append(arg)
  95. elif isinstance(arg, Population):
  96. # absorb entire population
  97. self.organisms.extend(arg)
  98. else:
  99. raise TypeError(
  100. "can only add Organism or Population objects")
  101. self.sorted = False
  102. def __add__(self, other):
  103. """
  104. Produce a whole new population consisting of an aggregate
  105. of this population and the other population's members
  106. """
  107. return Population(self, other)
  108. def getRandom(self, items=None):
  109. """
  110. randomly select one of the given items
  111. (or one of this population's members, if items
  112. not given).
  113. Favours fitter members
  114. """
  115. if items == None:
  116. items = self.organisms
  117. nitems = len(items)
  118. n2items = nitems * nitems
  119. # pick one parent randomly, favouring fittest
  120. idx = int(sqrt(randrange(n2items)))
  121. return items[nitems - idx - 1]
  122. def gen(self, nfittest=None, nchildren=None):
  123. """
  124. Executes a generation of the population.
  125. This consists of:
  126. - producing 'nchildren' children, parented by members
  127. randomly selected with preference for the fittest
  128. - culling the children to the fittest 'nfittest' members
  129. - killing off the parents, and replacing them with the
  130. children
  131. Read the source code to study the method of probabilistic
  132. selection.
  133. """
  134. if not nfittest:
  135. nfittest = self.childCull
  136. if not nchildren:
  137. nchildren = self.childCount
  138. children = []
  139. # add in some new random organisms, if required
  140. if self.numNewOrganisms:
  141. #print "adding %d new organisms" % self.numNewOrganisms
  142. for i in xrange(self.numNewOrganisms):
  143. self.add(self.species())
  144. # we use square root to skew the selection probability to
  145. # the fittest
  146. # get in order, if not already
  147. self.sort()
  148. nadults = len(self)
  149. n2adults = nadults * nadults
  150. # statistical survey
  151. #stats = {}
  152. #for j in xrange(nchildren):
  153. # stats[j] = 0
  154. # wild orgy, have lots of children
  155. for i in xrange(nchildren):
  156. # pick one parent randomly, favouring fittest
  157. idx1 = idx2 = int(sqrt(randrange(n2adults)))
  158. parent1 = self[-idx1]
  159. # pick another parent, distinct from the first parent
  160. while idx2 == idx1:
  161. idx2 = int(sqrt(randrange(n2adults)))
  162. parent2 = self[-idx2]
  163. #print "picking items %s, %s of %s" % (
  164. # nadults - idx1 - 1,
  165. # nadults - idx2 - 1,
  166. # nadults)
  167. #stats[nadults - idx1 - 1] += 1
  168. #stats[nadults - idx2 - 1] += 1
  169. # get it on, and store the child
  170. child1, child2 = parent1 + parent2
  171. # mutate kids if required
  172. if self.mutateAfterMating:
  173. child1 = child1.mutate()
  174. child2 = child2.mutate()
  175. children.extend([child1, child2])
  176. # if incestuous, add in best adults
  177. if self.incest:
  178. children.extend(self[:self.incest])
  179. for i, child in enumerate(children):
  180. #~ print 'child %s' % i
  181. child.prepare_fitness()
  182. children.sort()
  183. # and add in some mutants, a proportion of the children
  184. # with a bias toward the fittest
  185. if not self.mutateAfterMating:
  186. nchildren = len(children)
  187. n2children = nchildren * nchildren
  188. mutants = []
  189. numMutants = int(nchildren * self.mutants)
  190. if 1:
  191. for i in xrange(numMutants):
  192. # pick one parent randomly, favouring fittest
  193. idx = int(sqrt(randrange(n2children)))
  194. #child = children[nchildren - idx - 1]
  195. child = children[-idx]
  196. mutant = child.mutate()
  197. mutant.prepare_fitness()
  198. mutants.append(mutant)
  199. else:
  200. for i in xrange(numMutants):
  201. mutant = children[i].mutate()
  202. mutant.prepare_fitness()
  203. mutants.append(mutant)
  204. children.extend(mutants)
  205. children.sort()
  206. #print "added %s mutants" % numMutants
  207. # sort the children by fitness
  208. # take the best 'nfittest', make them the new population
  209. self.organisms[:] = children[:nfittest]
  210. self.sorted = True
  211. #return stats
  212. def __repr__(self):
  213. """
  214. crude human-readable dump of population's members
  215. """
  216. return str(self.organisms)
  217. def __getitem__(self, n):
  218. """
  219. Return the nth member of this population,
  220. which we guarantee to be sorted in order from
  221. fittest first
  222. """
  223. self.sort()
  224. return self.organisms[n]
  225. def __len__(self):
  226. """
  227. return the number of organisms in this population
  228. """
  229. return len(self.organisms)
  230. def fitness(self):
  231. """
  232. returns the average fitness value for the population
  233. """
  234. fitnesses = map(lambda org: org.get_fitness(), self.organisms)
  235. return sum(fitnesses)/len(fitnesses)
  236. def best(self):
  237. """
  238. returns the fittest member of the population
  239. """
  240. self.sort()
  241. return self[0]
  242. def sort(self):
  243. """
  244. Sorts this population in order of fitness, with
  245. the fittest first.
  246. We keep track of whether this population is in order
  247. of fitness, so we don't perform unnecessary and
  248. costly sorting
  249. """
  250. if not self.sorted:
  251. for organism in self.organisms:
  252. organism.prepare_fitness()
  253. self.organisms.sort()
  254. self.sorted = True
  255. # methods for loading/saving to/from xml
  256. def xmlDumpSelf(self, doc, parent):
  257. """
  258. Writes out the contents of this population
  259. into the xml tree
  260. """
  261. # create population element
  262. pop = doc.createElement("population")
  263. parent.appendChild(pop)
  264. # set population class details
  265. pop.setAttribute("class", self.__class__.__name__)
  266. pop.setAttribute("module", self.__class__.__module__)
  267. # set population params as xml tag attributes
  268. pop.setAttribute("childCull", str(self.childCull))
  269. pop.setAttribute("childCount", str(self.childCount))
  270. # dump out organisms
  271. for org in self.organisms:
  272. org.xmlDumpSelf(doc, pop)