organism.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. """
  2. Implements classes for entire organisms
  3. Organisms produce Gametes (think sperm/egg) via
  4. the .split() method.
  5. Organisms can be mated by the '+' operator, which
  6. produces a child organism.
  7. Subclasses of Organism must override the following methods:
  8. - fitness - returns a float value representing the
  9. organism's fitness - a value from 0.0 to infinity, where
  10. lower is better
  11. Refer to module pygene.prog for organism classes for genetic
  12. programming.
  13. """
  14. from random import random, randrange, choice
  15. from gene import BaseGene, rndPair
  16. from gamete import Gamete
  17. from xmlio import PGXmlMixin
  18. class BaseOrganism(PGXmlMixin):
  19. """
  20. Base class for genetic algo and genetic programming
  21. organisms
  22. Best not use this directly, but rather use or subclass from
  23. one of:
  24. - Organism
  25. - MendelOrganism
  26. - ProgOrganism
  27. """
  28. def __add__(self, partner):
  29. """
  30. Allows '+' operator for sexual reproduction
  31. Returns a whole new organism object, whose
  32. gene pair for each gene name are taken as one
  33. gene randomly selected from each parent
  34. """
  35. return self.mate(partner)
  36. def mate(self, partner):
  37. """
  38. Mates this organism with another organism to
  39. produce an entirely new organism
  40. Override this in subclasses
  41. """
  42. raise Exception("method 'mate' not implemented")
  43. def fitness(self):
  44. """
  45. Return the fitness level of this organism, as a float.
  46. Usually instead of this method a caching method 'get_fitness'
  47. is used, which calls this method always only once on an
  48. organism.
  49. Should return a number from 0.0 to infinity, where
  50. 0.0 means 'perfect'
  51. Organisms should evolve such that 'fitness' converges
  52. to zero.
  53. This method must be overridden
  54. """
  55. raise Exception("Method 'fitness' not implemented")
  56. def prepare_fitness(self):
  57. """
  58. Is called on all organisms before asking them for their
  59. fitness. This allows to calculate fitness using a parallel
  60. processing which is started by prepare_fitness, and finalized
  61. in 'fitness' method. By default this method does nothing.
  62. Organisms using this method should usually take care to call
  63. it themselves in case it wasn't called before hand.
  64. """
  65. pass
  66. def get_fitness(self):
  67. """
  68. Return fitness from the cache, and if needed - calculate it.
  69. """
  70. if self.fitness_cache is not None:
  71. return self.fitness_cache
  72. else:
  73. self.fitness_cache = self.fitness()
  74. return self.fitness_cache
  75. def duel(self, opponent):
  76. """
  77. Duels this organism against an opponent
  78. Returns -1 if this organism loses, 0 if it's
  79. a tie, or 1 if this organism wins
  80. """
  81. #print "BaseOrganism.duel: opponent=%s" % str(opponent)
  82. return cmp(self.get_fitness(), opponent.get_fitness())
  83. def __cmp__(self, other):
  84. """
  85. Convenience method which invokes duel
  86. Allows lists of organisms to be sorted
  87. """
  88. return self.duel(other)
  89. def __repr__(self):
  90. """
  91. Delivers a minimal string representation
  92. of this organism.
  93. Override if needed
  94. """
  95. return "<%s:%s>" % (self.__class__.__name__, self.get_fitness())
  96. def mutate(self):
  97. """
  98. Implement the mutation phase
  99. Must be overridden
  100. """
  101. raise Exception("method 'mutate' not implemented")
  102. def dump(self):
  103. """
  104. Produce a detailed human-readable report on
  105. this organism and its structure
  106. """
  107. raise Exception("method 'dump' not implemented")
  108. def xmlDumpSelf(self, doc, parent):
  109. """
  110. Dumps out this object's contents into an xml tree
  111. Arguments:
  112. - doc - an xml.dom.minidom.Document object
  113. - parent - an xml.dom.minidom.Element parent, being
  114. the node into which this node should be placed
  115. """
  116. raise Exception("method xmlDumpSelf not implemented")
  117. def xmlDumpAttribs(self, elem):
  118. """
  119. Dump out the custom attributes of this
  120. organism
  121. elem is an xml.dom.minidom.element object
  122. """
  123. class Organism(BaseOrganism):
  124. """
  125. Simple genetic algorithms organism
  126. Contains only single genes, not pairs (ie, single-helix)
  127. Note - all organisms are hermaphrodites, which
  128. can reproduce by mating with another.
  129. In this implementation, there is no gender.
  130. Class variables (to override) are:
  131. - genome - a dict mapping gene names to gene classes
  132. - mutateOneOnly - default False - dictates whether mutation
  133. affects one randomly chosen gene unconditionally, or
  134. all genes subject to the genes' individual mutation settings
  135. - crossoverRate - default .5 - proportion of genes to
  136. split out to first child in each pair resulting from
  137. a mating
  138. Python operators supported:
  139. - + - mates two organism instances together, producing a child
  140. - [] - returns the value of the gene of a given name
  141. - <, <=, >, >= - compares fitness value to that of another instance
  142. """
  143. # dict which maps genotype names to gene classes
  144. genome = {}
  145. # dictates whether mutation affects one randomly chosen
  146. # gene unconditionally, or all genes subject to the genes'
  147. # own mutation settings
  148. mutateOneOnly = False
  149. # proportion of genes to split out to first
  150. # child
  151. crossoverRate = 0.5
  152. def __init__(self, **kw):
  153. """
  154. Initialises this organism randomly,
  155. or from a set of named gene keywords
  156. Arguments:
  157. - gamete1, gamete2 - a pair of gametes from which
  158. to take the genes comprising the new organism.
  159. May be omitted.
  160. Keywords:
  161. - keyword names are gene names within the organism's
  162. genome, and values are either:
  163. - instances of a Gene subclass, or
  164. - a Gene subclass (in which case the class will
  165. be instantiated to form a random gene object)
  166. Any gene names in the genome, which aren't given in the
  167. constructor keywords, will be added as random instances
  168. of the respective gene class. (Recall that all Gene subclasses
  169. can be instantiated with no arguments to create a random
  170. valued gene).
  171. """
  172. # the set of genes which comprise this organism
  173. self.genes = {}
  174. # Cache fitness
  175. self.fitness_cache = None
  176. # remember the gene count
  177. self.numgenes = len(self.genome)
  178. # we're being fed a set of zero or more genes
  179. for name, cls in self.genome.items():
  180. # set genepair from given arg, or default to a
  181. # new random instance of the gene
  182. gene = kw.get(name, cls)
  183. # if we're handed a gene class instead of a gene object
  184. # we need to instantiate the gene class
  185. # to form the needed gene object
  186. if type(gene) == type and issubclass(gene, BaseGene):
  187. gene = gene()
  188. elif not isinstance(gene, BaseGene):
  189. # If it wasn't a subclass check if it's an instance
  190. raise Exception(
  191. "object given as gene %s %s is not a gene" % (
  192. name, repr(gene)))
  193. # all good - add in the gene to our genotype
  194. self.genes[name] = gene
  195. def copy(self):
  196. """
  197. returns a deep copy of this organism
  198. """
  199. genes = {}
  200. for name, gene in self.genes.items():
  201. genes[name] = gene.copy()
  202. return self.__class__(**genes)
  203. def mate(self, partner):
  204. """
  205. Mates this organism with another organism to
  206. produce two entirely new organisms via random choice
  207. of genes from this or the partner
  208. """
  209. genotype1 = {}
  210. genotype2 = {}
  211. # gene by gene, we assign our and partner's genes randomly
  212. for name, cls in self.genome.items():
  213. ourGene = self.genes.get(name, None)
  214. if not ourGene:
  215. ourGene = cls()
  216. partnerGene = self.genes.get(name, None)
  217. if not partnerGene:
  218. partnerGene = cls()
  219. # randomly assign genes to first or second child
  220. if random() < self.crossoverRate:
  221. genotype1[name] = ourGene
  222. genotype2[name] = partnerGene
  223. else:
  224. genotype1[name] = partnerGene
  225. genotype2[name] = ourGene
  226. # got the genotypes, now create the child organisms
  227. child1 = self.__class__(**genotype1)
  228. child2 = self.__class__(**genotype2)
  229. # done
  230. return (child1, child2)
  231. def __getitem__(self, item):
  232. """
  233. allows shorthand for querying the phenotype
  234. of this organism
  235. """
  236. return self.genes[item].value
  237. def phenotype(self, geneName=None):
  238. """
  239. Returns the phenotype resulting from a
  240. given gene, OR the total phenotype resulting
  241. from all the genes
  242. tries to invoke a child class' method
  243. called 'phen_<name>'
  244. """
  245. # if no gene name specified, build up an entire
  246. # phenotype dict
  247. if geneName == None:
  248. phenotype = {}
  249. for name, cls in self.genome.items():
  250. val = self.phenotype(name)
  251. if not phenotype.has_key(name):
  252. phenotype[name] = []
  253. phenotype[name].append(val)
  254. # got the whole phenotype now
  255. return phenotype
  256. # just getting the phenotype for one gene pair
  257. return self.genes[geneName]
  258. def mutate(self):
  259. """
  260. Implement the mutation phase, invoking
  261. the stochastic mutation method on each
  262. component gene
  263. Does not affect this organism, but returns a mutated
  264. copy of it
  265. """
  266. mutant = self.copy()
  267. if self.mutateOneOnly:
  268. # unconditionally mutate just one gene
  269. gene = choice(mutant.genes.values())
  270. gene.mutate()
  271. else:
  272. # conditionally mutate all genes
  273. for gene in mutant.genes.values():
  274. gene.maybeMutate()
  275. return mutant
  276. def dump(self):
  277. """
  278. Produce a detailed human-readable report on
  279. this organism, its genotype and phenotype
  280. """
  281. print "Organism %s:" % self.__class__.__name__
  282. print " Fitness: %s" % self.get_fitness()
  283. for k,v in self.genes.items():
  284. print " Gene: %s = %s" % (k, v)
  285. def xmlDumpSelf(self, doc, parent):
  286. """
  287. Dumps out this object's contents into an xml tree
  288. Arguments:
  289. - doc - an xml.dom.minidom.Document object
  290. - parent - an xml.dom.minidom.Element parent, being
  291. the node into which this node should be placed
  292. """
  293. orgtag = doc.createElement("organism")
  294. parent.appendChild(orgtag)
  295. self.xmlDumpClass(orgtag)
  296. self.xmlDumpAttribs(orgtag)
  297. # now dump out the constituent genes
  298. for name, cls in self.genome.items():
  299. # create a named genepair tag to contain genes
  300. pairtag = doc.createElement("genepair")
  301. orgtag.appendChild(pairtag)
  302. pairtag.setAttribute("name", name)
  303. # now write out genes
  304. gene = self.genes[name]
  305. #print "self.genes[%s] = %s" % (
  306. # name,
  307. # pair.__class__
  308. # )
  309. gene.xmlDumpSelf(doc, pairtag)
  310. class MendelOrganism(BaseOrganism):
  311. """
  312. Classical Mendelian genetic organism
  313. Contains a pair of genes for each gene in the genome
  314. Organisms contain a set of pairs of genes, where the
  315. genes of each pair must be of the same type.
  316. Class variables (to override) are:
  317. - genome - a dict mapping gene names to gene classes
  318. - mutateOneOnly - default False - if set, then the
  319. mutation phase will mutate exactly one of the genepairs
  320. in the genotype, randomly selected. If False, then
  321. apply mutation to all genes, subject to individual genes'
  322. mutation settings
  323. Python operators supported:
  324. - + - mates two organism instances together, producing a child
  325. - [] - returns the phenotype produced by the gene pair of a given name
  326. - <, <=, >, >= - compares fitness value to that of another instance
  327. """
  328. # dict which maps genotype names to gene classes
  329. genome = {}
  330. # dictates whether mutation affects one randomly chosen
  331. # gene unconditionally, or all genes subject to the genes'
  332. # own mutation settings
  333. mutateOneOnly = False
  334. def __init__(self, gamete1=None, gamete2=None, **kw):
  335. """
  336. Initialises this organism from either two gametes,
  337. or from a set of named gene keywords
  338. Arguments:
  339. - gamete1, gamete2 - a pair of gametes from which
  340. to take the genes comprising the new organism.
  341. May be omitted.
  342. Keywords:
  343. - keyword names are gene names within the organism's
  344. genome, and values are either:
  345. - a tuple containing two instances of a Gene
  346. subclass, or
  347. - a Gene subclass (in which case the class will
  348. be instantiated twice to form a random gene pair)
  349. Any gene names in the genome, which aren't given in the
  350. constructor keywords, will be added as random instances
  351. of the respective gene class. (Recall that all Gene subclasses
  352. can be instantiated with no arguments to create a random
  353. valued gene).
  354. """
  355. # the set of genes which comprise this organism
  356. self.genes = {}
  357. # Cache fitness
  358. self.fitness_cache = None
  359. # remember the gene count
  360. self.numgenes = len(self.genome)
  361. if gamete1 and gamete2:
  362. # create this organism from sexual reproduction
  363. for name, cls in self.genome.items():
  364. self.genes[name] = (
  365. gamete1[name].copy(),
  366. gamete2[name].copy(),
  367. )
  368. # and apply mutation
  369. #self.mutate()
  370. # done, easy as that
  371. return
  372. # other case - we're being fed a set of zero or more genes
  373. for name, cls in self.genome.items():
  374. # set genepair from given arg, or default to a
  375. # new random instance of the gene
  376. genepair = kw.get(name, cls)
  377. # if we're handed a gene class instead of a tuple
  378. # of 2 genes, we need to instantiate the gene class
  379. # to form the needed tuple
  380. if type(genepair) == type and issubclass(genepair, BaseGene):
  381. genepair = rndPair(genepair)
  382. else:
  383. # we're given a tuple; validate the gene pair
  384. try:
  385. gene1, gene2 = genepair
  386. except:
  387. raise TypeError(
  388. "constructor keyword values must be tuple of 2 Genes")
  389. if not isinstance(gene1, BaseGene):
  390. raise Exception(
  391. "object %s is not a gene" % repr(gene1))
  392. if not isinstance(gene2, BaseGene):
  393. raise Exception(
  394. "object %s is not a gene" % repr(gene2))
  395. # all good - add in the gene pair to our genotype
  396. self.genes[name] = genepair
  397. def copy(self):
  398. """
  399. returns a deep copy of this organism
  400. """
  401. genes = {}
  402. for name, genepair in self.genes.items():
  403. genes[name] = (genepair[0].copy(), genepair[1].copy())
  404. return self.__class__(**genes)
  405. def split(self):
  406. """
  407. Produces a Gamete object from random
  408. splitting of component gene pairs
  409. """
  410. genes1 = {}
  411. genes2 = {}
  412. for name, cls in self.genome.items():
  413. # fetch the pair of genes of that name
  414. genepair = self.genes[name]
  415. if randrange(0,2):
  416. genes1[name] = genepair[0]
  417. genes2[name] = genepair[1]
  418. else:
  419. genes1[name] = genepair[1]
  420. genes2[name] = genepair[0]
  421. # and pick one randomly
  422. #genes[name] = choice(genepair)
  423. gamete1 = Gamete(self.__class__, **genes1)
  424. gamete2 = Gamete(self.__class__, **genes2)
  425. return (gamete1, gamete2)
  426. def mate(self, partner):
  427. """
  428. Mates this organism with another organism to
  429. produce two entirely new organisms via mendelian crossover
  430. """
  431. #return self.split() + partner.split()
  432. ourGametes = self.split()
  433. partnerGametes = partner.split()
  434. child1 = self.__class__(ourGametes[0], partnerGametes[1])
  435. child2 = self.__class__(ourGametes[1], partnerGametes[0])
  436. return (child1, child2)
  437. # old single-child impl
  438. child = self.__class__(self.split(), partner.split())
  439. # look what the stork brought in!
  440. return child
  441. def __getitem__(self, item):
  442. """
  443. allows shorthand for querying the phenotype
  444. of this organism
  445. """
  446. return self.phenotype(item)
  447. def phenotype(self, geneName=None):
  448. """
  449. Returns the phenotype resulting from a
  450. given gene, OR the total phenotype resulting
  451. from all the genes
  452. tries to invoke a child class' method
  453. called 'phen_<name>'
  454. """
  455. # if no gene name specified, build up an entire
  456. # phenotype dict
  457. if geneName == None:
  458. phenotype = {}
  459. for name, cls in self.genome.items():
  460. val = self.phenotype(name)
  461. if not phenotype.has_key(name):
  462. phenotype[name] = []
  463. phenotype[name].append(val)
  464. # got the whole phenotype now
  465. return phenotype
  466. # just getting the phenotype for one gene pair
  467. if not isinstance(geneName, str):
  468. geneName = str(geneName)
  469. try:
  470. #return sum(self.genes[geneName])
  471. genes = self.genes[geneName]
  472. return genes[0] + genes[1]
  473. except:
  474. #print "self.genes[%s] = %s" % (geneName, self.genes[geneName])
  475. raise
  476. # get the genes in question
  477. gene1, gene2 = self.genes[geneName]
  478. # try to find a specialised phenotype
  479. # calculation method
  480. methname = 'phen_' + geneName
  481. meth = getattr(self, methname, None)
  482. if meth:
  483. # got the method - invoke it
  484. return meth(gene1, gene2)
  485. else:
  486. # no specialised methods, apply the genes'
  487. # combination methods
  488. return gene1 + gene2
  489. def mutate(self):
  490. """
  491. Implement the mutation phase, invoking
  492. the stochastic mutation method on each
  493. component gene
  494. Does not affect this organism, but returns a mutated
  495. copy of it
  496. """
  497. mutant = self.copy()
  498. if self.mutateOneOnly:
  499. # unconditionally mutate just one gene
  500. genepair = choice(mutant.genes.values())
  501. genepair[0].mutate()
  502. genepair[1].mutate()
  503. else:
  504. # conditionally mutate all genes
  505. for gene_a, gene_b in mutant.genes.values():
  506. gene_a.maybeMutate()
  507. gene_b.maybeMutate()
  508. return mutant
  509. def dump(self):
  510. """
  511. Produce a detailed human-readable report on
  512. this organism, its genotype and phenotype
  513. """
  514. print "Organism %s:" % self.__class__.__name__
  515. print " Fitness: %s" % self.get_fitness()
  516. for k,v in self.genes.items():
  517. print " Gene: %s" % k
  518. print " Phenotype: %s" % self[k]
  519. print " Genotype:"
  520. print " %s" % v[0]
  521. print " %s" % v[1]
  522. def xmlDumpSelf(self, doc, parent):
  523. """
  524. Dumps out this object's contents into an xml tree
  525. Arguments:
  526. - doc - an xml.dom.minidom.Document object
  527. - parent - an xml.dom.minidom.Element parent, being
  528. the node into which this node should be placed
  529. """
  530. orgtag = doc.createElement("organism")
  531. parent.appendChild(orgtag)
  532. self.xmlDumpClass(orgtag)
  533. self.xmlDumpAttribs(orgtag)
  534. # now dump out the constituent genes
  535. for name, cls in self.genome.items():
  536. # create a named genepair tag to contain genes
  537. pairtag = doc.createElement("genepair")
  538. orgtag.appendChild(pairtag)
  539. pairtag.setAttribute("name", name)
  540. # now write out genes
  541. pair = self.genes[name]
  542. #print "self.genes[%s] = %s" % (
  543. # name,
  544. # pair.__class__
  545. # )
  546. for gene in pair:
  547. gene.xmlDumpSelf(doc, pairtag)