prog.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. #@+leo-ver=4
  2. #@+node:@file pygene/prog.py
  3. """
  4. Implements genetic programming organisms
  5. """
  6. #@+others
  7. #@+node:imports
  8. from random import random, randrange, choice
  9. from math import sqrt
  10. from organism import BaseOrganism
  11. from xmlio import PGXmlMixin
  12. #@-node:imports
  13. #@+node:class BaseNode
  14. class BaseNode:
  15. """
  16. Base class for genetic programming nodes
  17. """
  18. #@ @+others
  19. #@+node:calc
  20. def calc(self, **vars):
  21. """
  22. evaluates this node, plugging vars into
  23. the nodes
  24. """
  25. raise Exception("method 'calc' not implemented")
  26. #@-node:calc
  27. #@-others
  28. #@-node:class BaseNode
  29. #@+node:class FuncNode
  30. class FuncNode(BaseNode):
  31. """
  32. node which holds a function and its argument nodes
  33. """
  34. #@ @+others
  35. #@+node:__init__
  36. def __init__(self, org, depth, name=None, children=None):
  37. """
  38. creates this func node
  39. """
  40. self.org = org
  41. if name == None:
  42. # randomly choose a func
  43. name, func, nargs = choice(org.funcsList)
  44. else:
  45. # lookup func in organism
  46. func, nargs = org.funcsDict[name]
  47. # and fill in the args, from given, or randomly
  48. if not children:
  49. children = [org.genNode(depth+1) for i in xrange(nargs)]
  50. self.name = name
  51. self.func = func
  52. self.nargs = nargs
  53. self.children = children
  54. #@-node:__init__
  55. #@+node:calc
  56. def calc(self, **vars):
  57. """
  58. evaluates this node, plugging vars into
  59. the nodes
  60. """
  61. args = []
  62. for child in self.children:
  63. #print "FuncNode.calc: child %s dump:" % (
  64. # child.__class__,
  65. # )
  66. #child.dump()
  67. arg = child.calc(**vars)
  68. #print "child returned %s" % repr(arg)
  69. args.append(arg)
  70. #print "FuncNode.calc: name=%s func=%s vars=%s args=%s" % (
  71. # self.name,
  72. # self.func,
  73. # vars,
  74. # args
  75. # )
  76. return self.func(*args)
  77. #@-node:calc
  78. #@+node:dump
  79. def dump(self, level=0):
  80. indents = " " * level
  81. #print indents + "func:" + self.name
  82. print "%s%s" % (indents, self.name)
  83. for child in self.children:
  84. child.dump(level+1)
  85. #@-node:dump
  86. #@+node:copy
  87. def copy(self, doSplit=False):
  88. """
  89. Copies this node and recursively its children, returning
  90. the copy
  91. if doSplit is true, then
  92. cuts off a piece of the tree, to support
  93. the recombination phase of mating with another program
  94. returns a quadruple:
  95. - copy - a copy of this node
  96. - fragment - fragment to be given to mate
  97. - lst - list within copy tree to which fragment
  98. from mate should be written
  99. - idx - index within the lst at which the fragment
  100. should be written
  101. if doSplit is false, then the last 3 tuple items will be None
  102. """
  103. if not doSplit:
  104. # easy case - split has already occurred elsewhere
  105. # within the tree, so just clone the kids without
  106. # splitting
  107. clonedChildren = \
  108. [child.copy() for child in self.children]
  109. fragment = None
  110. lst = None
  111. idx = None
  112. # now ready to instantiate clone
  113. copy = FuncNode(self.org, 0, self.name, clonedChildren)
  114. return copy
  115. # choose a child of this node that we might split
  116. childIdx = randrange(0, self.nargs)
  117. childToSplit = self.children[childIdx]
  118. # if child is a terminal, we *must* split here.
  119. # if child is not terminal, randomly choose whether
  120. # to split here
  121. if random() < 0.33 \
  122. or isinstance(childToSplit, TerminalNode):
  123. # split at this node, and just copy the kids
  124. clonedChildren = \
  125. [child.copy() for child in self.children]
  126. # now ready to instantiate clone
  127. copy = FuncNode(self.org, 0, self.name, clonedChildren)
  128. return copy, childToSplit, self.children, childIdx
  129. else:
  130. # delegate the split down to selected child
  131. clonedChildren = []
  132. for i in xrange(self.nargs):
  133. child = self.children[i]
  134. if (i == childIdx):
  135. # chosen child
  136. (clonedChild,fragment,lst,idx) = child.copy(True)
  137. else:
  138. # just clone without splitting
  139. clonedChild = child.copy()
  140. clonedChildren.append(clonedChild)
  141. # now ready to instantiate clone
  142. copy = FuncNode(self.org, 0, self.name, clonedChildren)
  143. return copy, fragment, lst, idx
  144. #@-node:copy
  145. #@+node:mutate
  146. def mutate(self, depth):
  147. """
  148. randomly mutates either this tree, or a child
  149. """
  150. # 2 in 3 chance of mutating a child of this node
  151. if random() > 0.33:
  152. child = choice(self.children)
  153. if not isinstance(child, TerminalNode):
  154. child.mutate(depth+1)
  155. return
  156. # mutate this node - replace one of its children
  157. mutIdx = randrange(0, self.nargs)
  158. self.children[mutIdx] = self.org.genNode(depth+1)
  159. #print "mutate: depth=%s" % depth
  160. #@-node:mutate
  161. #@-others
  162. #@-node:class FuncNode
  163. #@+node:class TerminalNode
  164. class TerminalNode(BaseNode):
  165. """
  166. Holds a terminal value
  167. """
  168. #@ @+others
  169. #@-others
  170. #@-node:class TerminalNode
  171. #@+node:class ConstNode
  172. class ConstNode(TerminalNode):
  173. """
  174. Holds a constant value
  175. """
  176. #@ @+others
  177. #@+node:__init__
  178. def __init__(self, org, value=None):
  179. """
  180. """
  181. self.org = org
  182. if value == None:
  183. value = choice(org.consts)
  184. self.value = value
  185. #@nonl
  186. #@-node:__init__
  187. #@+node:calc
  188. def calc(self, **vars):
  189. """
  190. evaluates this node, returns value
  191. """
  192. # easy
  193. return self.value
  194. #@-node:calc
  195. #@+node:dump
  196. def dump(self, level=0):
  197. indents = " " * level
  198. #print "%sconst: {%s}" % (indents, self.value)
  199. print "%s{%s}" % (indents, self.value)
  200. #@-node:dump
  201. #@+node:copy
  202. def copy(self):
  203. """
  204. clone this node
  205. """
  206. return ConstNode(self.org, self.value)
  207. #@-node:copy
  208. #@-others
  209. #@-node:class ConstNode
  210. #@+node:class VarNode
  211. class VarNode(TerminalNode):
  212. """
  213. Holds a variable
  214. """
  215. #@ @+others
  216. #@+node:__init__
  217. def __init__(self, org, name=None):
  218. """
  219. Inits this node as a var placeholder
  220. """
  221. self.org = org
  222. if name == None:
  223. name = choice(org.vars)
  224. self.name = name
  225. #@-node:__init__
  226. #@+node:calc
  227. def calc(self, **vars):
  228. """
  229. Calculates val of this node
  230. """
  231. val = vars.get(self.name, 0.0)
  232. #print "VarNode.calc: name=%s val=%s vars=%s" % (
  233. # self.name,
  234. # val,
  235. # vars,
  236. # )
  237. return val
  238. #@-node:calc
  239. #@+node:dump
  240. def dump(self, level=0):
  241. indents = " " * level
  242. #print indents + "var {" + self.name + "}"
  243. print "%s{%s}" % (indents, self.name)
  244. #@-node:dump
  245. #@+node:copy
  246. def copy(self):
  247. """
  248. clone this node
  249. """
  250. return VarNode(self.org, self.name)
  251. #@-node:copy
  252. #@-others
  253. #@-node:class VarNode
  254. #@+node:class ProgOrganismMetaclass
  255. class ProgOrganismMetaclass(type):
  256. """
  257. a metaclass which analyses class attribs
  258. of a ProgOrganism subclass, and builds the
  259. list of functions and terminals
  260. """
  261. #@ @+others
  262. #@+node:__init__
  263. def __init__(cls, name, bases, dict):
  264. """
  265. Create the ProgOrganism class object
  266. """
  267. # parent constructor
  268. object.__init__(cls, name, bases, dict)
  269. # get the funcs, consts and vars class attribs
  270. funcs = dict['funcs']
  271. consts = dict['consts']
  272. vars = dict['vars']
  273. # process the funcs
  274. funcsList = []
  275. funcsDict = {}
  276. for name, func in funcs.items():
  277. funcsList.append((name, func, func.func_code.co_argcount))
  278. funcsDict[name] = (func, func.func_code.co_argcount)
  279. cls.funcsList = funcsList
  280. cls.funcsDict = funcsDict
  281. #@-node:__init__
  282. #@-others
  283. #@-node:class ProgOrganismMetaclass
  284. #@+node:class ProgOrganism
  285. class ProgOrganism(BaseOrganism):
  286. """
  287. Implements an organism for genetic programming
  288. Introspects to discover functions and terminals.
  289. You should add the folling class attribs:
  290. - funcs - a dictionary of funcs, names are func
  291. names, values are callable objects
  292. - vars - a list of variable names
  293. - consts - a list of constant values
  294. """
  295. #@ @+others
  296. #@+node:attribs
  297. __metaclass__ = ProgOrganismMetaclass
  298. funcs = {}
  299. vars = []
  300. consts = []
  301. # maximum tree depth when generating randomly
  302. maxDepth = 4
  303. # probability of a mutation occurring
  304. mutProb = 0.01
  305. #@-node:attribs
  306. #@+node:__init__
  307. def __init__(self, root=None):
  308. """
  309. Creates this organism
  310. """
  311. if root == None:
  312. root = self.genNode()
  313. self.tree = root
  314. #@-node:__init__
  315. #@+node:mate
  316. def mate(self, mate):
  317. """
  318. Perform recombination of subtree elements
  319. """
  320. # get copy of self, plus fragment and location details
  321. ourRootCopy, ourFrag, ourList, ourIdx = self.split()
  322. # ditto for mate
  323. mateRootCopy, mateFrag, mateList, mateIdx = mate.split()
  324. # swap the fragments
  325. ourList[ourIdx] = mateFrag
  326. mateList[mateIdx] = ourFrag
  327. # and return both progeny
  328. child1 = self.__class__(ourRootCopy)
  329. child2 = self.__class__(mateRootCopy)
  330. return (child1, child2)
  331. #@-node:mate
  332. #@+node:mutate
  333. def mutate(self):
  334. """
  335. Mutates this organism's node tree
  336. returns the mutant
  337. """
  338. mutant = self.copy()
  339. mutant.tree.mutate(1)
  340. return mutant
  341. #@-node:mutate
  342. #@+node:split
  343. def split(self):
  344. """
  345. support for recombination, returns a tuple
  346. with four values:
  347. - root - a copy of the tree, except for the fragment
  348. to be swapped
  349. - subtree - the subtree fragment to be swapped
  350. - lst - a list within the tree, containing the
  351. fragment
  352. - idx - index within the list where mate's fragment
  353. should be written
  354. """
  355. # otherwise, delegate the split down the tree
  356. copy, subtree, lst, idx = self.tree.copy(True)
  357. return (copy, subtree, lst, idx)
  358. #@-node:split
  359. #@+node:copy
  360. def copy(self):
  361. """
  362. returns a deep copy of this organism
  363. """
  364. try:
  365. return self.__class__(self.tree)
  366. except:
  367. print "self.__class__ = %s" % self.__class__
  368. raise
  369. #@-node:copy
  370. #@+node:dump
  371. def dump(self, node=None, level=1):
  372. """
  373. prints out this organism's node tree
  374. """
  375. print "organism:"
  376. self.tree.dump(1)
  377. #@-node:dump
  378. #@+node:genNode
  379. def genNode(self, depth=1):
  380. """
  381. Randomly generates a node to build in
  382. to this organism
  383. """
  384. if depth > 1 and (depth >= self.initDepth or flipCoin()):
  385. # not root, and either maxed depth, or 50-50 chance
  386. if flipCoin():
  387. # choose a var
  388. return VarNode(self)
  389. else:
  390. return ConstNode(self)
  391. # either root, or not maxed, or 50-50 chance
  392. return FuncNode(self, depth)
  393. #@-node:genNode
  394. #@+node:xmlDumpSelf
  395. def xmlDumpSelf(self, doc, parent):
  396. """
  397. Dumps out this object's contents into an xml tree
  398. Arguments:
  399. - doc - an xml.dom.minidom.Document object
  400. - parent - an xml.dom.minidom.Element parent, being
  401. the node into which this node should be placed
  402. """
  403. raise Exception("method xmlDumpSelf not implemented")
  404. #@-node:xmlDumpSelf
  405. #@+node:fitness
  406. def fitness(self):
  407. """
  408. Return the fitness level of this organism, as a float
  409. Should return a number from 0.0 to infinity, where
  410. 0.0 means 'perfect'
  411. Organisms should evolve such that 'fitness' converges
  412. to zero.
  413. This method must be overridden
  414. In your override, you should generate a set of values,
  415. either deterministically or randomly, and pass each
  416. value to both .testFunc() and .calculate(), comparing
  417. the results and using this to calculate the fitness
  418. """
  419. raise Exception("Method 'fitness' not implemented")
  420. #@-node:fitness
  421. #@+node:testFunc
  422. def testFunc(self, **kw):
  423. """
  424. this is the 'reference function' toward which
  425. organisms are trying to evolve
  426. You must override this in your organism subclass
  427. """
  428. raise Exception("method 'testFunc' not implemented")
  429. #@-node:testFunc
  430. #@+node:calc
  431. def calc(self, **vars):
  432. """
  433. Executes this program organism, using the given
  434. keyword parameters
  435. You shouldn't need to override this
  436. """
  437. #print "org.calc: vars=%s" % str(vars)
  438. return self.tree.calc(**vars)
  439. #@-node:calc
  440. #@-others
  441. #@-node:class ProgOrganism
  442. #@+node:funcs
  443. # util funcs
  444. #@+others
  445. #@+node:flipCoin
  446. def flipCoin():
  447. """
  448. randomly returns True/False
  449. """
  450. return choice((True, False))
  451. #@-node:flipCoin
  452. #@-others
  453. #@-node:funcs
  454. #@-others
  455. #@-node:@file pygene/prog.py
  456. #@-leo