├── .gitignore ├── Chromosome.py ├── Gene Expression Programming.ipynb ├── GeneExpressionProgram.py ├── lib ├── README ├── __init__.py └── anytree │ ├── __init__.py │ ├── dotexport.py │ ├── iterators.py │ ├── node.py │ ├── render.py │ ├── resolver.py │ └── walker.py ├── main.py └── tests ├── ChromosomeTests.py └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | __pycache__/ 4 | .ipynb_checkpoints/ 5 | -------------------------------------------------------------------------------- /Chromosome.py: -------------------------------------------------------------------------------- 1 | from lib.anytree.node import Node 2 | from lib.anytree.render import RenderTree 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from random import randint 7 | from warnings import warn 8 | 9 | 10 | class Chromosome: 11 | 12 | # Functions and Terminals are shared by all chromosomes 13 | functions = dict() 14 | terminals = list() 15 | constants = dict() 16 | ephemeral_random_constants_range = (-1, 1) 17 | linking_function = None 18 | 19 | # length of head of chromosome 20 | num_genes = 3 21 | head_length = 6 22 | length = 39 23 | 24 | # list of real-valued tuples of the form (x, f(x)) 25 | fitness_cases = [] 26 | max_fitness = None 27 | 28 | 29 | def __init__(self, genes: list): 30 | 31 | # do not let chromosomes be defined without first defining their functions, terminals, and head length 32 | if not Chromosome.functions: 33 | raise ValueError("Chromosome class has no functions associated with it.") 34 | if len(Chromosome.terminals) == 0: 35 | raise ValueError("Chromosome class has no terminals associated with it.") 36 | if Chromosome.length is None: 37 | raise ValueError("Chromosome class has no length defined.") 38 | if Chromosome.head_length is None: 39 | raise ValueError("Chromosome class has no head length defined.") 40 | if Chromosome.linking_function is None and len(genes) > 1: 41 | raise ValueError("Multigenic chromosome defined with no linking function.") 42 | if len(genes) != Chromosome.num_genes: 43 | raise ValueError("Number of genes does not match excpected value in class level variable.") 44 | if "?" in Chromosome.terminals and Chromosome.ephemeral_random_constants_range is None: 45 | raise ValueError("Must define ephemeral random constants range if using ephemeral random constants.") 46 | 47 | # initialize chromosomes 48 | self.genes = genes 49 | self.trees = [] 50 | self._values_ = {} 51 | self._fitness_ = None 52 | self.ephemeral_random_constants = list(np.random.uniform(*Chromosome.ephemeral_random_constants_range, size=Chromosome.length)) 53 | 54 | 55 | # TODO - put informative error message when terminal_values doesn't have enough entries 56 | def evaluate(self, terminal_values: dict) -> float: 57 | """ 58 | Returns the result of evaluating the given chromosome for specified fitness cases. 59 | 60 | :param terminal_values: dictionary mapping all present terminal symbols to real values 61 | :return: real valued result of evaluating the chromosome 62 | """ 63 | 64 | # memoize value in case the chromosome was already evaluated 65 | value_fingerprint = tuple(sorted(terminal_values.items())) 66 | if value_fingerprint in self._values_: 67 | return self._values_[value_fingerprint] 68 | 69 | # build expression trees for each gene if not already built 70 | if len(self.trees) == 0: 71 | self.trees = [Chromosome.build_tree(gene) for gene in self.genes] 72 | 73 | # link expression trees if the chromosome is multigenic, otherwise use first tree 74 | if self.num_genes > 1: 75 | expression_tree = Chromosome.link(*self.trees) 76 | else: 77 | expression_tree = self.trees[0] 78 | 79 | erc_index = 0 80 | 81 | # recursive inorder tree traversal 82 | def inorder(start: Node) -> float: 83 | nonlocal terminal_values, erc_index 84 | if start.name in Chromosome.terminals: 85 | if start.name == "?": 86 | erc_index += 1 87 | return self.ephemeral_random_constants[erc_index - 1] 88 | if start.name in Chromosome.constants: 89 | return Chromosome.constants[start.name] 90 | return int(start.name) if start.name.isdigit() else terminal_values[start.name] 91 | if start.name in Chromosome.functions: 92 | return Chromosome.functions[start.name]["f"](*[inorder(node) for node in start.children]) 93 | 94 | try: 95 | self._values_[value_fingerprint] = inorder(expression_tree) 96 | if isinstance(self._values_[value_fingerprint], np.complex): 97 | raise TypeError 98 | # ZeroDivisionError if tree does something like x/(y-y), TypeError if the takes square root of a negative. 99 | except (ZeroDivisionError, TypeError): 100 | self._values_[value_fingerprint] = np.nan 101 | 102 | # noinspection PyTypeChecker 103 | return self._values_[value_fingerprint] 104 | 105 | 106 | def fitness(self) -> float: 107 | """ 108 | Getter for fitness property to make sure we aren't grabbing uncalculated fitnesses 109 | 110 | :return: fitness of chromosome, or raise a warning and return 0 if fitness hasn't been calculated 111 | """ 112 | if self._fitness_ is not None: 113 | return self._fitness_ 114 | warn("Fitness of chromosome has not been properly calculated. Returning 0.") 115 | return 0 116 | 117 | 118 | def print_tree(self) -> None: 119 | """ 120 | Use AnyTree to display a Chromosome's expression tree(s) 121 | 122 | :return: void 123 | """ 124 | for t in range(len(self.trees)): 125 | print("Tree %d" % t) 126 | for pre, _, node in RenderTree(self.trees[t]): 127 | print("\t%s%s" % (pre, node.name)) 128 | print(self.ephemeral_random_constants) 129 | 130 | def plot_solution(self, objective_function, x_min: float, x_max: float, 131 | avg_fitnesses: list, best_fitnesses: list, variable_name: str) -> None: 132 | 133 | """ 134 | Mostly unused, handy for plotting symbolic regression results though 135 | 136 | :param objective_function: ground truth function to plot 137 | :param x_min: minimum x value of plot 138 | :param x_max: maximum x value of plot 139 | :param avg_fitnesses: list of average fitness values by generation 140 | :param best_fitnesses: list of best fitness values by generation 141 | :return: void 142 | """ 143 | 144 | if objective_function is not None: 145 | # set up subplots 146 | plt.subplots(1, 2, figsize=(16, 8)) 147 | 148 | # Objective function vs Discovered function plot 149 | xs = np.linspace(x_min, x_max, 100) 150 | plt.subplot(1, 2, 1) 151 | plt.title("Discovered function vs. Objective function") 152 | plt.plot(xs, [objective_function(x) for x in xs], 153 | linewidth=2, linestyle='dashed', color='black', label="Objective") 154 | plt.plot(xs, [self.evaluate({variable_name: x}) for x in xs], 155 | linewidth=2, color='blue', label="Discovered") 156 | plt.legend(loc="upper left") 157 | 158 | # Fitness over time plot 159 | plt.subplot(1, 2, 2) 160 | 161 | plt.title("Fitness by Generation") 162 | plt.plot(range(len(avg_fitnesses)), avg_fitnesses, label="Average") 163 | plt.plot(range(len(best_fitnesses)), best_fitnesses, label="Best") 164 | plt.legend(loc="upper left") 165 | plt.show() 166 | 167 | else: 168 | plt.subplots(1, 1, figsize=(8, 8)) 169 | plt.title("Fitness by Generation") 170 | plt.plot(range(len(avg_fitnesses)), avg_fitnesses, label="Average") 171 | plt.plot(range(len(best_fitnesses)), best_fitnesses, label="Best") 172 | plt.legend(loc="upper left") 173 | plt.show() 174 | 175 | 176 | @staticmethod 177 | def build_tree(gene: str) -> Node: 178 | """ 179 | Constructs an expression tree from a gene. 180 | 181 | :param gene: gene to turn into expression tree 182 | :return: anytree Node of the root of the tree 183 | """ 184 | 185 | # shortcut to get the number of arguments to a function 186 | def args(f: str) -> int: 187 | return Chromosome.functions[f]["args"] if f in Chromosome.functions else 0 188 | 189 | # recursively build chromosome tree 190 | def grab_children(parent: Node, current_level = 1): 191 | nonlocal levels 192 | if current_level < len(levels): 193 | nargs = args(parent.name) 194 | for i in range(nargs): 195 | current_node = Node(levels[current_level][i], parent=parent) 196 | grab_children(parent=current_node, current_level=current_level + 1) 197 | if current_level < len(levels) - 1: 198 | levels[current_level + 1] = levels[current_level + 1][args(current_node.name):] 199 | 200 | # build each level of the tree 201 | levels = [gene[0]] 202 | index = 0 203 | while index < len(gene) and sum([args(f) for f in levels[-1]]) != 0: 204 | nargs = sum([args(f) for f in levels[-1]]) 205 | levels.append(gene[index + 1: index + 1 + nargs]) 206 | index += nargs 207 | 208 | # intialize tree and parse 209 | tree = Node(gene[0]) 210 | grab_children(tree) 211 | return tree 212 | 213 | 214 | @staticmethod 215 | # TODO - verify recursive linking with non-commutative linking functions (e.g. -) 216 | def link(*args) -> Node: 217 | """ 218 | Links two trees at their roots using the specified linking function. 219 | Linking function must take as many arguments as number of args provided. 220 | 221 | :param args: expression trees to link. Must be at least as many expression trees as linking function has arguments. 222 | :return: expression tree with tree1 and tree2 as subtrees 223 | """ 224 | 225 | if Chromosome.linking_function not in Chromosome.functions: 226 | raise ValueError("Linking function is not defined in Chromosome.functions.") 227 | if not all([isinstance(arg, Node) for arg in args]): 228 | raise TypeError("Can only link expression trees.") 229 | 230 | nargs = Chromosome.functions[Chromosome.linking_function]["args"] 231 | 232 | def link_recursive(*args) -> Node: 233 | root = Node(Chromosome.linking_function) 234 | if len(args) == nargs: 235 | for tree in args: 236 | tree.parent = root 237 | return root 238 | else: 239 | return link_recursive(link_recursive(*args[:nargs]), *args[nargs:]) 240 | 241 | return link_recursive(*args) 242 | 243 | 244 | @staticmethod 245 | # TODO - calculate using numpy arrays for speed 246 | def absolute_fitness(M: float, *args) -> np.ndarray: 247 | """ 248 | Calculate absolute fitness of an arbitrary number of Chromosomes. 249 | 250 | :param M: range of fitness function over domain 251 | :param args: any number of gene objects 252 | :return: list of fitnesses of corresponding chromosomes 253 | """ 254 | fitnesses = [] 255 | for chromosome in args: 256 | # memoize fitness values 257 | if chromosome._fitness_ is not None: 258 | fitnesses.append(chromosome._fitness_) 259 | else: 260 | fitness = 0 261 | for j in range(len(Chromosome.fitness_cases)): 262 | C_ij = chromosome.evaluate(Chromosome.fitness_cases[j][0]) 263 | # assign any chromosome that divides by zero a fitness value of zero 264 | if type(C_ij) == np.complex or np.isnan(C_ij) or np.isinf(C_ij) or np.isneginf(C_ij): 265 | fitness = 0 266 | break 267 | T_j = Chromosome.fitness_cases[j][1] 268 | fitness += M - abs(C_ij - T_j) 269 | chromosome._fitness_ = fitness 270 | fitnesses.append(fitness) 271 | return np.asarray(fitnesses) 272 | 273 | 274 | @staticmethod 275 | # TODO - calculate using numpy arrays for speed 276 | def relative_fitness(M: float, *args) -> np.ndarray: 277 | """ 278 | Calculate relative fitness of an arbitrary number of genes. 279 | 280 | :param M: range of fitness function over domain 281 | :param args: any number of gene objects 282 | :return: list of fitnesses of corresponding genes 283 | """ 284 | fitnesses = [] 285 | for chromosome in args: 286 | # memoize fitness values 287 | if chromosome._fitness_ is not None: 288 | fitnesses.append(chromosome._fitness_) 289 | else: 290 | fitness = 0 291 | for j in range(len(Chromosome.fitness_cases)): 292 | C_ij = chromosome.evaluate(Chromosome.fitness_cases[j][0]) 293 | T_j = Chromosome.fitness_cases[j][1] 294 | fitness += M - 100*abs(C_ij / T_j - 1) 295 | chromosome._fitness_ = fitness 296 | fitnesses.append(fitness) 297 | return np.asarray(fitnesses) 298 | 299 | 300 | @staticmethod 301 | def inv_squared_error(*args) -> np.ndarray: 302 | """ 303 | Classical 1/(1+(squared error) fitness value. 304 | 305 | :param args: list of chromosomes to calculate fitness of 306 | :return: ndarray of fitness values for each given chromosome 307 | """ 308 | fitnesses = [] 309 | for chromosome in args: 310 | # memoize fitness values 311 | if chromosome._fitness_ is not None: 312 | fitnesses.append(chromosome._fitness_) 313 | else: 314 | fitness = 0 315 | for j in range(len(Chromosome.fitness_cases)): 316 | C_ij = chromosome.evaluate(Chromosome.fitness_cases[j][0]) 317 | if type(C_ij) == np.complex or np.isnan(C_ij) or np.isinf(C_ij) or np.isneginf(C_ij): 318 | fitness = np.inf 319 | break 320 | T_j = Chromosome.fitness_cases[j][1] 321 | fitness += (C_ij - T_j)**2 322 | chromosome._fitness_ = 1.0/(1+fitness) 323 | fitnesses.append(chromosome._fitness_) 324 | return np.asarray(fitnesses) 325 | 326 | 327 | @staticmethod 328 | def centralized_inv_squared_error(center: float, dimension: str, *args) -> np.ndarray: 329 | """ 330 | Mostly unused, a fitness function that focuses on error near a given point. 331 | 332 | :param center: point around which the fitness values are more important 333 | :param dimension: which dimension of the fitness case to use, in the case of multivariate functions 334 | :param args: any chromosomes to calculate fitness of 335 | :return: ndarray of fitness values 336 | """ 337 | fitnesses = [] 338 | for chromosome in args: 339 | # memoize fitness values 340 | if chromosome._fitness_ is not None: 341 | fitnesses.append(chromosome._fitness_) 342 | else: 343 | fitness = 0 344 | for j in range(len(Chromosome.fitness_cases)): 345 | C_ij = chromosome.evaluate(Chromosome.fitness_cases[j][0]) 346 | if type(C_ij) == np.complex or np.isnan(C_ij) or np.isinf(C_ij) or np.isneginf(C_ij): 347 | fitness = np.inf 348 | break 349 | T_j = Chromosome.fitness_cases[j][1] 350 | fitness += abs(C_ij - T_j)**(1/abs(Chromosome.fitness_cases[j][0][dimension] - center)) 351 | chromosome._fitness_ = 1.0 / (1 + fitness) 352 | fitnesses.append(chromosome._fitness_) 353 | return np.asarray(fitnesses) 354 | 355 | 356 | @staticmethod 357 | def generate_random_gene() -> str: 358 | """ 359 | Generates one random gene based on settings specified in Chromosome class. 360 | :return: string of valid characters 361 | """ 362 | possible_chars = list(Chromosome.functions.keys()) + Chromosome.terminals 363 | head = "".join([possible_chars[randint(0, len(possible_chars) - 1)] for _ in range(Chromosome.head_length)]) 364 | tail = "".join([Chromosome.terminals[randint(0, len(Chromosome.terminals) - 1)] for _ in range(Chromosome.length - Chromosome.head_length)]) 365 | return head + tail 366 | 367 | 368 | @staticmethod 369 | def generate_random_individual() -> 'Chromosome': 370 | """ 371 | Generates one random individual based on settings specified in Chromosome class. 372 | :return: new Chromosome 373 | """ 374 | return Chromosome([Chromosome.generate_random_gene() for _ in range(Chromosome.num_genes)]) 375 | -------------------------------------------------------------------------------- /Gene Expression Programming.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Gene Expression Programming" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## Imports\n", 15 | "External libraries used:\n", 16 | "* AnyTree (packaged locally, no installation required)\n", 17 | "* MatPlotLib\n", 18 | "* Numpy" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "collapsed": true, 26 | "scrolled": true 27 | }, 28 | "outputs": [], 29 | "source": [ 30 | "from copy import deepcopy\n", 31 | "from lib.anytree.node import Node\n", 32 | "from lib.anytree.render import RenderTree\n", 33 | "import matplotlib.pyplot as plt\n", 34 | "import numpy as np\n", 35 | "from random import random, randint, shuffle\n", 36 | "from warnings import warn\n", 37 | "%matplotlib inline" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "## Chromosome Class - Container for a list of genes (strings)\n", 45 | "Contains methods to...\n", 46 | "* Build an expression tree from a gene\n", 47 | "* Evaluate an expression tree given fitness cases and terminal values\n", 48 | "* Display the chromosome's expression tree to the console\n", 49 | "* Link chromosomes\n", 50 | "* Calculate fitness (several ways)\n", 51 | "* Generate random genes/chromosomes" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": { 58 | "collapsed": true, 59 | "scrolled": true 60 | }, 61 | "outputs": [], 62 | "source": [ 63 | "class Chromosome:\n", 64 | "\n", 65 | " # Functions and Terminals are shared by all chromosomes\n", 66 | " functions = dict()\n", 67 | " terminals = list()\n", 68 | " constants = dict()\n", 69 | " ephemeral_random_constants_range = (-1, 1)\n", 70 | " linking_function = None\n", 71 | "\n", 72 | " # length of head of chromosome\n", 73 | " num_genes = 3\n", 74 | " head_length = 6\n", 75 | " length = 39\n", 76 | "\n", 77 | " # list of real-valued tuples of the form (x, f(x))\n", 78 | " fitness_cases = []\n", 79 | " max_fitness = None\n", 80 | "\n", 81 | "\n", 82 | " def __init__(self, genes: list):\n", 83 | "\n", 84 | " # do not let chromosomes be defined without first defining their functions, terminals, and head length\n", 85 | " if not Chromosome.functions:\n", 86 | " raise ValueError(\"Chromosome class has no functions associated with it.\")\n", 87 | " if len(Chromosome.terminals) == 0:\n", 88 | " raise ValueError(\"Chromosome class has no terminals associated with it.\")\n", 89 | " if Chromosome.length is None:\n", 90 | " raise ValueError(\"Chromosome class has no length defined.\")\n", 91 | " if Chromosome.head_length is None:\n", 92 | " raise ValueError(\"Chromosome class has no head length defined.\")\n", 93 | " if Chromosome.linking_function is None and len(genes) > 1:\n", 94 | " raise ValueError(\"Multigenic chromosome defined with no linking function.\")\n", 95 | " if len(genes) != Chromosome.num_genes:\n", 96 | " raise ValueError(\"Number of genes does not match excpected value in class level variable.\")\n", 97 | " if \"?\" in Chromosome.terminals and Chromosome.ephemeral_random_constants_range is None:\n", 98 | " raise ValueError(\"Must define ephemeral random constants range if using ephemeral random constants.\")\n", 99 | "\n", 100 | " # initialize chromosomes\n", 101 | " self.genes = genes\n", 102 | " self.trees = []\n", 103 | " self._values_ = {}\n", 104 | " self._fitness_ = None\n", 105 | " self.ephemeral_random_constants = list(np.random.uniform(*Chromosome.ephemeral_random_constants_range, size=Chromosome.length))\n", 106 | "\n", 107 | "\n", 108 | " # TODO - put informative error message when terminal_values doesn't have enough entries\n", 109 | " def evaluate(self, terminal_values: dict) -> float:\n", 110 | " \"\"\"\n", 111 | " Returns the result of evaluating the given chromosome for specified fitness cases.\n", 112 | "\n", 113 | " :param terminal_values: dictionary mapping all present terminal symbols to real values\n", 114 | " :return: real valued result of evaluating the chromosome\n", 115 | " \"\"\"\n", 116 | "\n", 117 | " # memoize value in case the chromosome was already evaluated\n", 118 | " value_fingerprint = tuple(sorted(terminal_values.items()))\n", 119 | " if value_fingerprint in self._values_:\n", 120 | " return self._values_[value_fingerprint]\n", 121 | "\n", 122 | " # build expression trees for each gene if not already built\n", 123 | " if len(self.trees) == 0:\n", 124 | " self.trees = [Chromosome.build_tree(gene) for gene in self.genes]\n", 125 | "\n", 126 | " # link expression trees if the chromosome is multigenic, otherwise use first tree\n", 127 | " if self.num_genes > 1:\n", 128 | " expression_tree = Chromosome.link(*self.trees)\n", 129 | " else:\n", 130 | " expression_tree = self.trees[0]\n", 131 | "\n", 132 | " erc_index = 0\n", 133 | "\n", 134 | " # recursive inorder tree traversal\n", 135 | " def inorder(start: Node) -> float:\n", 136 | " nonlocal terminal_values, erc_index\n", 137 | " if start.name in Chromosome.terminals:\n", 138 | " if start.name == \"?\":\n", 139 | " erc_index += 1\n", 140 | " return self.ephemeral_random_constants[erc_index - 1]\n", 141 | " if start.name in Chromosome.constants:\n", 142 | " return Chromosome.constants[start.name]\n", 143 | " return int(start.name) if start.name.isdigit() else terminal_values[start.name]\n", 144 | " if start.name in Chromosome.functions:\n", 145 | " return Chromosome.functions[start.name][\"f\"](*[inorder(node) for node in start.children])\n", 146 | "\n", 147 | " try:\n", 148 | " self._values_[value_fingerprint] = inorder(expression_tree)\n", 149 | " if isinstance(self._values_[value_fingerprint], np.complex):\n", 150 | " raise TypeError\n", 151 | " # ZeroDivisionError if tree does something like x/(y-y), TypeError if the takes square root of a negative.\n", 152 | " except (ZeroDivisionError, TypeError):\n", 153 | " self._values_[value_fingerprint] = np.nan\n", 154 | "\n", 155 | " # noinspection PyTypeChecker\n", 156 | " return self._values_[value_fingerprint]\n", 157 | "\n", 158 | "\n", 159 | " def fitness(self) -> float:\n", 160 | " \"\"\"\n", 161 | " Getter for fitness property to make sure we aren't grabbing uncalculated fitnesses\n", 162 | "\n", 163 | " :return: fitness of chromosome, or raise a warning and return 0 if fitness hasn't been calculated\n", 164 | " \"\"\"\n", 165 | " if self._fitness_ is not None:\n", 166 | " return self._fitness_\n", 167 | " warn(\"Fitness of chromosome has not been properly calculated. Returning 0.\")\n", 168 | " return 0\n", 169 | "\n", 170 | "\n", 171 | " def print_tree(self) -> None:\n", 172 | " \"\"\"\n", 173 | " Use AnyTree to display a Chromosome's expression tree(s)\n", 174 | "\n", 175 | " :return: void\n", 176 | " \"\"\"\n", 177 | " for t in range(len(self.trees)):\n", 178 | " print(\"Tree %d\" % t)\n", 179 | " for pre, _, node in RenderTree(self.trees[t]):\n", 180 | " print(\"\\t%s%s\" % (pre, node.name))\n", 181 | " print(self.ephemeral_random_constants)\n", 182 | "\n", 183 | " def plot_solution(self, objective_function, x_min: float, x_max: float,\n", 184 | " avg_fitnesses: list, best_fitnesses: list, variable_name: str) -> None:\n", 185 | "\n", 186 | " \"\"\"\n", 187 | " Mostly unused, handy for plotting symbolic regression results though\n", 188 | "\n", 189 | " :param objective_function: ground truth function to plot\n", 190 | " :param x_min: minimum x value of plot\n", 191 | " :param x_max: maximum x value of plot\n", 192 | " :param avg_fitnesses: list of average fitness values by generation\n", 193 | " :param best_fitnesses: list of best fitness values by generation\n", 194 | " :return: void\n", 195 | " \"\"\"\n", 196 | "\n", 197 | " if objective_function is not None:\n", 198 | " # set up subplots\n", 199 | " plt.subplots(1, 2, figsize=(16, 8))\n", 200 | "\n", 201 | " # Objective function vs Discovered function plot\n", 202 | " xs = np.linspace(x_min, x_max, 100)\n", 203 | " plt.subplot(1, 2, 1)\n", 204 | " plt.title(\"Discovered function vs. Objective function\")\n", 205 | " plt.plot(xs, [objective_function(x) for x in xs],\n", 206 | " linewidth=2, linestyle='dashed', color='black', label=\"Objective\")\n", 207 | " plt.plot(xs, [self.evaluate({variable_name: x}) for x in xs],\n", 208 | " linewidth=2, color='blue', label=\"Discovered\")\n", 209 | " plt.legend(loc=\"upper left\")\n", 210 | "\n", 211 | " # Fitness over time plot\n", 212 | " plt.subplot(1, 2, 2)\n", 213 | "\n", 214 | " plt.title(\"Fitness by Generation\")\n", 215 | " plt.plot(range(len(avg_fitnesses)), avg_fitnesses, label=\"Average\")\n", 216 | " plt.plot(range(len(best_fitnesses)), best_fitnesses, label=\"Best\")\n", 217 | " plt.legend(loc=\"upper left\")\n", 218 | " plt.show()\n", 219 | "\n", 220 | " else:\n", 221 | " plt.subplots(1, 1, figsize=(8, 8))\n", 222 | " plt.title(\"Fitness by Generation\")\n", 223 | " plt.plot(range(len(avg_fitnesses)), avg_fitnesses, label=\"Average\")\n", 224 | " plt.plot(range(len(best_fitnesses)), best_fitnesses, label=\"Best\")\n", 225 | " plt.legend(loc=\"upper left\")\n", 226 | " plt.show()\n", 227 | "\n", 228 | "\n", 229 | " @staticmethod\n", 230 | " def build_tree(gene: str) -> Node:\n", 231 | " \"\"\"\n", 232 | " Constructs an expression tree from a gene.\n", 233 | "\n", 234 | " :param gene: gene to turn into expression tree\n", 235 | " :return: anytree Node of the root of the tree\n", 236 | " \"\"\"\n", 237 | "\n", 238 | " # shortcut to get the number of arguments to a function\n", 239 | " def args(f: str) -> int:\n", 240 | " return Chromosome.functions[f][\"args\"] if f in Chromosome.functions else 0\n", 241 | "\n", 242 | " # recursively build chromosome tree\n", 243 | " def grab_children(parent: Node, current_level = 1):\n", 244 | " nonlocal levels\n", 245 | " if current_level < len(levels):\n", 246 | " nargs = args(parent.name)\n", 247 | " for i in range(nargs):\n", 248 | " current_node = Node(levels[current_level][i], parent=parent)\n", 249 | " grab_children(parent=current_node, current_level=current_level + 1)\n", 250 | " if current_level < len(levels) - 1:\n", 251 | " levels[current_level + 1] = levels[current_level + 1][args(current_node.name):]\n", 252 | "\n", 253 | " # build each level of the tree\n", 254 | " levels = [gene[0]]\n", 255 | " index = 0\n", 256 | " while index < len(gene) and sum([args(f) for f in levels[-1]]) != 0:\n", 257 | " nargs = sum([args(f) for f in levels[-1]])\n", 258 | " levels.append(gene[index + 1: index + 1 + nargs])\n", 259 | " index += nargs\n", 260 | "\n", 261 | " # intialize tree and parse\n", 262 | " tree = Node(gene[0])\n", 263 | " grab_children(tree)\n", 264 | " return tree\n", 265 | "\n", 266 | "\n", 267 | " @staticmethod\n", 268 | " # TODO - verify recursive linking with non-commutative linking functions (e.g. -)\n", 269 | " def link(*args) -> Node:\n", 270 | " \"\"\"\n", 271 | " Links two trees at their roots using the specified linking function.\n", 272 | " Linking function must take as many arguments as number of args provided.\n", 273 | "\n", 274 | " :param args: expression trees to link. Must be at least as many expression trees as linking function has arguments.\n", 275 | " :return: expression tree with tree1 and tree2 as subtrees\n", 276 | " \"\"\"\n", 277 | "\n", 278 | " if Chromosome.linking_function not in Chromosome.functions:\n", 279 | " raise ValueError(\"Linking function is not defined in Chromosome.functions.\")\n", 280 | " if not all([isinstance(arg, Node) for arg in args]):\n", 281 | " raise TypeError(\"Can only link expression trees.\")\n", 282 | "\n", 283 | " nargs = Chromosome.functions[Chromosome.linking_function][\"args\"]\n", 284 | "\n", 285 | " def link_recursive(*args) -> Node:\n", 286 | " root = Node(Chromosome.linking_function)\n", 287 | " if len(args) == nargs:\n", 288 | " for tree in args:\n", 289 | " tree.parent = root\n", 290 | " return root\n", 291 | " else:\n", 292 | " return link_recursive(link_recursive(*args[:nargs]), *args[nargs:])\n", 293 | "\n", 294 | " return link_recursive(*args)\n", 295 | "\n", 296 | "\n", 297 | " @staticmethod\n", 298 | " # TODO - calculate using numpy arrays for speed\n", 299 | " def absolute_fitness(M: float, *args) -> np.ndarray:\n", 300 | " \"\"\"\n", 301 | " Calculate absolute fitness of an arbitrary number of Chromosomes.\n", 302 | "\n", 303 | " :param M: range of fitness function over domain\n", 304 | " :param args: any number of gene objects\n", 305 | " :return: list of fitnesses of corresponding chromosomes\n", 306 | " \"\"\"\n", 307 | " fitnesses = []\n", 308 | " for chromosome in args:\n", 309 | " # memoize fitness values\n", 310 | " if chromosome._fitness_ is not None:\n", 311 | " fitnesses.append(chromosome._fitness_)\n", 312 | " else:\n", 313 | " fitness = 0\n", 314 | " for j in range(len(Chromosome.fitness_cases)):\n", 315 | " C_ij = chromosome.evaluate(Chromosome.fitness_cases[j][0])\n", 316 | " # assign any chromosome that divides by zero a fitness value of zero\n", 317 | " if type(C_ij) == np.complex or np.isnan(C_ij) or np.isinf(C_ij) or np.isneginf(C_ij):\n", 318 | " fitness = 0\n", 319 | " break\n", 320 | " T_j = Chromosome.fitness_cases[j][1]\n", 321 | " fitness += M - abs(C_ij - T_j)\n", 322 | " chromosome._fitness_ = fitness\n", 323 | " fitnesses.append(fitness)\n", 324 | " return np.asarray(fitnesses)\n", 325 | "\n", 326 | "\n", 327 | " @staticmethod\n", 328 | " # TODO - calculate using numpy arrays for speed\n", 329 | " def relative_fitness(M: float, *args) -> np.ndarray:\n", 330 | " \"\"\"\n", 331 | " Calculate relative fitness of an arbitrary number of genes.\n", 332 | "\n", 333 | " :param M: range of fitness function over domain\n", 334 | " :param args: any number of gene objects\n", 335 | " :return: list of fitnesses of corresponding genes\n", 336 | " \"\"\"\n", 337 | " fitnesses = []\n", 338 | " for chromosome in args:\n", 339 | " # memoize fitness values\n", 340 | " if chromosome._fitness_ is not None:\n", 341 | " fitnesses.append(chromosome._fitness_)\n", 342 | " else:\n", 343 | " fitness = 0\n", 344 | " for j in range(len(Chromosome.fitness_cases)):\n", 345 | " C_ij = chromosome.evaluate(Chromosome.fitness_cases[j][0])\n", 346 | " T_j = Chromosome.fitness_cases[j][1]\n", 347 | " fitness += M - 100*abs(C_ij / T_j - 1)\n", 348 | " chromosome._fitness_ = fitness\n", 349 | " fitnesses.append(fitness)\n", 350 | " return np.asarray(fitnesses)\n", 351 | "\n", 352 | "\n", 353 | " @staticmethod\n", 354 | " def inv_squared_error(*args) -> np.ndarray:\n", 355 | " \"\"\"\n", 356 | " Classical 1/(1+(squared error) fitness value.\n", 357 | "\n", 358 | " :param args: list of chromosomes to calculate fitness of\n", 359 | " :return: ndarray of fitness values for each given chromosome\n", 360 | " \"\"\"\n", 361 | " fitnesses = []\n", 362 | " for chromosome in args:\n", 363 | " # memoize fitness values\n", 364 | " if chromosome._fitness_ is not None:\n", 365 | " fitnesses.append(chromosome._fitness_)\n", 366 | " else:\n", 367 | " fitness = 0\n", 368 | " for j in range(len(Chromosome.fitness_cases)):\n", 369 | " C_ij = chromosome.evaluate(Chromosome.fitness_cases[j][0])\n", 370 | " if type(C_ij) == np.complex or np.isnan(C_ij) or np.isinf(C_ij) or np.isneginf(C_ij):\n", 371 | " fitness = np.inf\n", 372 | " break\n", 373 | " T_j = Chromosome.fitness_cases[j][1]\n", 374 | " fitness += (C_ij - T_j)**2\n", 375 | " chromosome._fitness_ = 1.0/(1+fitness)\n", 376 | " fitnesses.append(chromosome._fitness_)\n", 377 | " return np.asarray(fitnesses)\n", 378 | "\n", 379 | "\n", 380 | " @staticmethod\n", 381 | " def centralized_inv_squared_error(center: float, dimension: str, *args) -> np.ndarray:\n", 382 | " \"\"\"\n", 383 | " Mostly unused, a fitness function that focuses on error near a given point.\n", 384 | "\n", 385 | " :param center: point around which the fitness values are more important\n", 386 | " :param dimension: which dimension of the fitness case to use, in the case of multivariate functions\n", 387 | " :param args: any chromosomes to calculate fitness of\n", 388 | " :return: ndarray of fitness values\n", 389 | " \"\"\"\n", 390 | " fitnesses = []\n", 391 | " for chromosome in args:\n", 392 | " # memoize fitness values\n", 393 | " if chromosome._fitness_ is not None:\n", 394 | " fitnesses.append(chromosome._fitness_)\n", 395 | " else:\n", 396 | " fitness = 0\n", 397 | " for j in range(len(Chromosome.fitness_cases)):\n", 398 | " C_ij = chromosome.evaluate(Chromosome.fitness_cases[j][0])\n", 399 | " if type(C_ij) == np.complex or np.isnan(C_ij) or np.isinf(C_ij) or np.isneginf(C_ij):\n", 400 | " fitness = np.inf\n", 401 | " break\n", 402 | " T_j = Chromosome.fitness_cases[j][1]\n", 403 | " fitness += abs(C_ij - T_j)**(1/abs(Chromosome.fitness_cases[j][0][dimension] - center))\n", 404 | " chromosome._fitness_ = 1.0 / (1 + fitness)\n", 405 | " fitnesses.append(chromosome._fitness_)\n", 406 | " return np.asarray(fitnesses)\n", 407 | "\n", 408 | "\n", 409 | " @staticmethod\n", 410 | " def generate_random_gene() -> str:\n", 411 | " \"\"\"\n", 412 | " Generates one random gene based on settings specified in Chromosome class.\n", 413 | " :return: string of valid characters\n", 414 | " \"\"\"\n", 415 | " possible_chars = list(Chromosome.functions.keys()) + Chromosome.terminals\n", 416 | " head = \"\".join([possible_chars[randint(0, len(possible_chars) - 1)] for _ in range(Chromosome.head_length)])\n", 417 | " tail = \"\".join([Chromosome.terminals[randint(0, len(Chromosome.terminals) - 1)] for _ in range(Chromosome.length - Chromosome.head_length)])\n", 418 | " return head + tail\n", 419 | "\n", 420 | "\n", 421 | " @staticmethod\n", 422 | " def generate_random_individual() -> 'Chromosome':\n", 423 | " \"\"\"\n", 424 | " Generates one random individual based on settings specified in Chromosome class.\n", 425 | " :return: new Chromosome\n", 426 | " \"\"\"\n", 427 | " return Chromosome([Chromosome.generate_random_gene() for _ in range(Chromosome.num_genes)])\n" 428 | ] 429 | }, 430 | { 431 | "cell_type": "markdown", 432 | "metadata": {}, 433 | "source": [ 434 | "## GeneExpressionProgram Class - Container for Program Execution\n", 435 | "Why do you have a class where every method is static?\n", 436 | "\n", 437 | "Because I don't like polluting the global namespace, and I like grouping variables by their function.\n", 438 | "\n", 439 | "Main driver for Gene Expression Programming. Contains methods for:\n", 440 | "* Evolving a configured program\n", 441 | "* Roulette wheel selection\n", 442 | "* Mutation\n", 443 | "* All kinds of recombination\n", 444 | "* Plotting the results of evolution" 445 | ] 446 | }, 447 | { 448 | "cell_type": "code", 449 | "execution_count": null, 450 | "metadata": { 451 | "collapsed": true, 452 | "scrolled": true 453 | }, 454 | "outputs": [], 455 | "source": [ 456 | "class GeneExpressionProgram:\n", 457 | "\n", 458 | " ### Hyperparameters ###\n", 459 | " NUM_RUNS = 5\n", 460 | " NUM_GENERATIONS = 500\n", 461 | " POPULATION_SIZE = 100\n", 462 | " NUM_FITNESS_CASES = 10\n", 463 | " ERROR_TOLERANCE = 0.0000001\n", 464 | "\n", 465 | " ### Reproduction ###\n", 466 | " MUTATION_RATE = 0.051\n", 467 | " ONE_POINT_CROSSOVER_RATE, TWO_POINT_CROSSOVER_RATE, GENE_CROSSOVER_RATE = 0.2, 0.5, 0.1\n", 468 | " IS_TRANSPOSITION_RATE, IS_ELEMENTS_LENGTH = 0.1, [1, 2, 3]\n", 469 | " RIS_TRANSPOSITION_RATE, RIS_ELEMENTS_LENGTH = 0.1, [1, 2, 3]\n", 470 | " GENE_TRANSPOSITION_RATE = 0.1\n", 471 | "\n", 472 | " ### Fitness Evaluation ###\n", 473 | " OBJECTIVE_FUNCTION = None\n", 474 | " FITNESS_FUNCTION = None\n", 475 | " FITNESS_FUNCTION_ARGS = list()\n", 476 | " OBJECTIVE_MIN, OBJECTIVE_MAX = None, None\n", 477 | " FUNCTION_Y_RANGE = None\n", 478 | "\n", 479 | "\n", 480 | " def __init__(self):\n", 481 | " pass\n", 482 | "\n", 483 | "\n", 484 | " @staticmethod\n", 485 | " def evolve() -> (Chromosome, list, list):\n", 486 | " \"\"\"\n", 487 | " Execute Gene Expression Programming algorithm\n", 488 | "\n", 489 | " :return: tuple of:\n", 490 | " the best fit chromosome,\n", 491 | " list of average fitness by generation (for plotting),\n", 492 | " list of best fitness by generation (for plotting\n", 493 | " \"\"\"\n", 494 | "\n", 495 | " # create initial population\n", 496 | " population = [Chromosome.generate_random_individual() for _ in range(GeneExpressionProgram.POPULATION_SIZE)]\n", 497 | "\n", 498 | " generation = 0\n", 499 | " best_fit_individual = None\n", 500 | " average_fitness_by_generation = []\n", 501 | " best_fitness_by_generation = []\n", 502 | " while generation < GeneExpressionProgram.NUM_GENERATIONS:\n", 503 | "\n", 504 | " ### EVALUATION ###\n", 505 | "\n", 506 | " # calcluate fitnesses for population\n", 507 | " population_fitnesses = GeneExpressionProgram.FITNESS_FUNCTION(*GeneExpressionProgram.FITNESS_FUNCTION_ARGS, *population)\n", 508 | "\n", 509 | " # find best fit individual\n", 510 | " # noinspection PyTypeChecker\n", 511 | " best_fit_generation = population[np.argmax(population_fitnesses)]\n", 512 | " if generation == 0 or best_fit_individual.fitness() < best_fit_generation.fitness():\n", 513 | " best_fit_individual = deepcopy(best_fit_generation)\n", 514 | "\n", 515 | " # skip rest of loop if we have found optimal solution\n", 516 | " if abs(best_fit_individual.fitness() - Chromosome.max_fitness) <= GeneExpressionProgram.ERROR_TOLERANCE:\n", 517 | " average_fitness_generation = float(np.mean(population_fitnesses))\n", 518 | " average_fitness_by_generation.append(average_fitness_generation)\n", 519 | " best_fitness_by_generation.append(best_fit_individual.fitness())\n", 520 | " break\n", 521 | "\n", 522 | " next_generation = list()\n", 523 | "\n", 524 | "\n", 525 | " ### SELECTION (roulette wheel with simple elitism) ###\n", 526 | "\n", 527 | " # copy best individual to next generation\n", 528 | " next_generation.append(deepcopy(best_fit_individual))\n", 529 | "\n", 530 | " # select the rest of the next generation with roulette wheel selection\n", 531 | " all_parents = GeneExpressionProgram.roulette_wheel_selection(population, len(population))\n", 532 | "\n", 533 | " # Mutation\n", 534 | " all_parents = list(map(GeneExpressionProgram.mutate, all_parents))\n", 535 | "\n", 536 | " # IS Transposition\n", 537 | " all_parents = list(map(GeneExpressionProgram.is_transposition, all_parents))\n", 538 | "\n", 539 | " # RIS Transposition\n", 540 | " all_parents = list(map(GeneExpressionProgram.ris_transposition, all_parents))\n", 541 | "\n", 542 | " # Gene Transposition\n", 543 | " all_parents = list(map(GeneExpressionProgram.gene_transposition, all_parents))\n", 544 | "\n", 545 | " # Recombination\n", 546 | " shuffle(all_parents)\n", 547 | " for i in range(1, GeneExpressionProgram.POPULATION_SIZE, 2):\n", 548 | "\n", 549 | " # in case we don't have a pair to check for crossover, avoid index error\n", 550 | " if i + 1 >= GeneExpressionProgram.POPULATION_SIZE:\n", 551 | " next_generation.append(all_parents[i])\n", 552 | " break\n", 553 | "\n", 554 | " child1, child2 = all_parents[i], all_parents[i+1]\n", 555 | "\n", 556 | " # One-point Recombination\n", 557 | " if random() < GeneExpressionProgram.ONE_POINT_CROSSOVER_RATE:\n", 558 | " child1, child2 = GeneExpressionProgram.one_point_recombination(child1, child2)\n", 559 | "\n", 560 | " # Two-point Recombination\n", 561 | " elif random() < GeneExpressionProgram.TWO_POINT_CROSSOVER_RATE:\n", 562 | " child1, child2 = GeneExpressionProgram.two_point_recombination(child1, child2)\n", 563 | "\n", 564 | " # Gene Recombination\n", 565 | " elif random() < GeneExpressionProgram.GENE_CROSSOVER_RATE:\n", 566 | " child1, child2 = GeneExpressionProgram.gene_recombination(child1, child2)\n", 567 | "\n", 568 | " # Include children in next generation\n", 569 | " next_generation.append(child1)\n", 570 | " next_generation.append(child2)\n", 571 | "\n", 572 | " # prepare for next iteration\n", 573 | " population = next_generation\n", 574 | " generation += 1\n", 575 | "\n", 576 | " average_fitness_generation = float(np.mean(population_fitnesses))\n", 577 | " average_fitness_by_generation.append(average_fitness_generation)\n", 578 | " best_fitness_by_generation.append(best_fit_individual.fitness())\n", 579 | " print(\"Generation: %d\\tPopulation Size: %d\\tAverage Fitness: %.5f\\tBest Fitness (overall): %.5f\" %\n", 580 | " (generation, len(population), average_fitness_generation, best_fit_individual.fitness()))\n", 581 | "\n", 582 | " return best_fit_individual, average_fitness_by_generation, best_fitness_by_generation\n", 583 | "\n", 584 | "\n", 585 | " @staticmethod\n", 586 | " def random_search(num_generations: int, fitness_function: callable, fitness_function_args: list) -> (Chromosome, list, list):\n", 587 | " best = None\n", 588 | " best_fitness = 0\n", 589 | " average_fitnesses = []\n", 590 | " best_fitnesses = []\n", 591 | " for gen in range(num_generations):\n", 592 | " generation_fitnesses = []\n", 593 | " for individual in range(GeneExpressionProgram.POPULATION_SIZE):\n", 594 | " current = Chromosome.generate_random_individual()\n", 595 | " current_fitness = fitness_function(*fitness_function_args, current)\n", 596 | " generation_fitnesses.append(current_fitness)\n", 597 | " if best is None or best_fitness <= current_fitness:\n", 598 | " best = deepcopy(current)\n", 599 | " best_fitness = best.fitness()\n", 600 | " average_fitnesses.append(np.mean(generation_fitnesses))\n", 601 | " best_fitnesses.append(best_fitness)\n", 602 | " if best_fitness >= Chromosome.max_fitness:\n", 603 | " break\n", 604 | " return best, average_fitnesses, best_fitnesses\n", 605 | "\n", 606 | "\n", 607 | " @staticmethod\n", 608 | " def roulette_wheel_selection(chromosomes: list, n: int) -> list:\n", 609 | " \"\"\"\n", 610 | " Returns n randomly selected chromosomes using roulette wheel selection.\n", 611 | " Adapted from:\n", 612 | " https://stackoverflow.com/questions/2140787/select-k-random-elements-from-a-list-whose-elements-have-weights\n", 613 | "\n", 614 | " :param chromosomes: list of chromosomes to select from\n", 615 | " :param n: number of samples to retrieve\n", 616 | " :return: list of chosen chromosome(s)\n", 617 | " \"\"\"\n", 618 | " total = float(sum(c.fitness() for c in chromosomes))\n", 619 | " i = 0\n", 620 | " fitness = chromosomes[0].fitness()\n", 621 | " while n:\n", 622 | " x = total * (1 - random() ** (1.0 / n))\n", 623 | " total -= x\n", 624 | " while x > fitness:\n", 625 | " x -= fitness\n", 626 | " i += 1\n", 627 | " fitness = chromosomes[i].fitness()\n", 628 | " fitness -= x\n", 629 | " yield chromosomes[i]\n", 630 | " n -= 1\n", 631 | "\n", 632 | "\n", 633 | " @staticmethod\n", 634 | " def mutate(chromosome: Chromosome) -> Chromosome:\n", 635 | " \"\"\"\n", 636 | " Randomly mutate genes in a chromosome.\n", 637 | "\n", 638 | " :param chromosome: chromosome to mutate\n", 639 | " :return: mutated chromosome\n", 640 | " \"\"\"\n", 641 | "\n", 642 | " head_characters = list(Chromosome.functions.keys()) + Chromosome.terminals\n", 643 | "\n", 644 | " new_genes = []\n", 645 | " # for each gene (multigenic chromosomes)\n", 646 | " for gene in range(Chromosome.num_genes):\n", 647 | " new_gene = \"\"\n", 648 | " # for each character in the gene\n", 649 | " for i in range(len(chromosome.genes[gene])):\n", 650 | " # if we are to mutate it\n", 651 | " if random() < GeneExpressionProgram.MUTATION_RATE:\n", 652 | " # if we are mutating the head\n", 653 | " if i < Chromosome.head_length:\n", 654 | " new_gene += head_characters[randint(0, len(head_characters) - 1)]\n", 655 | " else:\n", 656 | " new_gene += Chromosome.terminals[randint(0, len(Chromosome.terminals) - 1)]\n", 657 | " else:\n", 658 | " new_gene += chromosome.genes[gene][i]\n", 659 | " new_genes.append(new_gene)\n", 660 | "\n", 661 | " # create new chromosome to ensure memoized fitness values are recalculated.\n", 662 | " new_chromosome = Chromosome(new_genes)\n", 663 | " new_chromosome.ephemeral_random_constants = chromosome.ephemeral_random_constants\n", 664 | "\n", 665 | " # mutate ephemeral random constants\n", 666 | " for constant in range(len(new_chromosome.ephemeral_random_constants)):\n", 667 | " if random() < GeneExpressionProgram.MUTATION_RATE:\n", 668 | " new_chromosome.ephemeral_random_constants[constant] = np.random.uniform(*Chromosome.ephemeral_random_constants_range)\n", 669 | "\n", 670 | " return new_chromosome\n", 671 | "\n", 672 | "\n", 673 | " @staticmethod\n", 674 | " def is_transposition(chromosome: Chromosome) -> Chromosome:\n", 675 | " \"\"\"\n", 676 | " Insertion Sequence transposition.\n", 677 | "\n", 678 | " :param chromosome: chromosome to perform IS transposition on\n", 679 | " :return: new chromosome\n", 680 | " \"\"\"\n", 681 | "\n", 682 | " # TODO - properly transpose ephemeral random constants\n", 683 | "\n", 684 | " if random() < GeneExpressionProgram.IS_TRANSPOSITION_RATE:\n", 685 | "\n", 686 | " # determine parameters of transposition\n", 687 | " length = np.random.choice(GeneExpressionProgram.IS_ELEMENTS_LENGTH)\n", 688 | " source_gene = randint(0, len(chromosome.genes) - 1)\n", 689 | " target_gene = randint(0, len(chromosome.genes) - 1)\n", 690 | " target_position = randint(1, Chromosome.head_length - length)\n", 691 | " sequence_start = randint(0, len(chromosome.genes[source_gene]))\n", 692 | "\n", 693 | " transposition_string = chromosome.genes[source_gene][sequence_start:min(Chromosome.length, sequence_start + length)]\n", 694 | "\n", 695 | " # make substitution\n", 696 | " new_chromosome = Chromosome(chromosome.genes)\n", 697 | " new_chromosome.ephemeral_random_constants = chromosome.ephemeral_random_constants\n", 698 | " new_chromosome.genes[target_gene] = new_chromosome.genes[target_gene][:target_position] + \\\n", 699 | " transposition_string + \\\n", 700 | " new_chromosome.genes[target_gene][target_position + length:]\n", 701 | "\n", 702 | " return new_chromosome\n", 703 | "\n", 704 | " else:\n", 705 | "\n", 706 | " return chromosome\n", 707 | "\n", 708 | "\n", 709 | " @staticmethod\n", 710 | " def ris_transposition(chromosome: Chromosome) -> Chromosome:\n", 711 | " \"\"\"\n", 712 | " Root Insertion Sequence transposition.\n", 713 | "\n", 714 | " :param chromosome: chromosome to perform RIS transposition on\n", 715 | " :return: new chromosome\n", 716 | " \"\"\"\n", 717 | "\n", 718 | " # TODO - properly transpose ephemeral random constants\n", 719 | "\n", 720 | " start_point = randint(0, Chromosome.head_length - 1)\n", 721 | " gene = randint(0, Chromosome.num_genes - 1)\n", 722 | " while start_point < Chromosome.head_length and chromosome.genes[gene][start_point] not in Chromosome.functions:\n", 723 | " start_point += 1\n", 724 | "\n", 725 | " if random() < GeneExpressionProgram.RIS_TRANSPOSITION_RATE and chromosome.genes[gene][start_point] in Chromosome.functions:\n", 726 | " ris_length = np.random.choice(GeneExpressionProgram.RIS_ELEMENTS_LENGTH)\n", 727 | " ris_string = chromosome.genes[gene][start_point:start_point+ris_length]\n", 728 | "\n", 729 | " new_chromosome = Chromosome(chromosome.genes)\n", 730 | " new_chromosome.ephemeral_random_constants = chromosome.ephemeral_random_constants\n", 731 | " old_head = new_chromosome.genes[gene][:Chromosome.head_length]\n", 732 | " new_head = old_head[:start_point] + ris_string + old_head[start_point:]\n", 733 | " new_chromosome.genes[gene] = new_head[:Chromosome.head_length] + new_chromosome.genes[gene][Chromosome.head_length:]\n", 734 | "\n", 735 | " return new_chromosome\n", 736 | "\n", 737 | " else:\n", 738 | "\n", 739 | " return chromosome\n", 740 | "\n", 741 | "\n", 742 | " @staticmethod\n", 743 | " def gene_transposition(chromosome: Chromosome) -> Chromosome:\n", 744 | " \"\"\"\n", 745 | " Gene Insertion Sequence transposition.\n", 746 | "\n", 747 | " :param chromosome: chromosome to perform gene transposition on\n", 748 | " :return: new chromosome\n", 749 | " \"\"\"\n", 750 | "\n", 751 | " # TODO - properly transpose ephemeral random constants\n", 752 | "\n", 753 | " if Chromosome.num_genes > 1 and random() < GeneExpressionProgram.GENE_TRANSPOSITION_RATE:\n", 754 | "\n", 755 | " index = randint(0, Chromosome.num_genes - 1)\n", 756 | " temp = chromosome.genes[index]\n", 757 | " chromosome.genes[index] = chromosome.genes[0]\n", 758 | " chromosome.genes[0] = temp\n", 759 | " new_chromosome = Chromosome(chromosome.genes)\n", 760 | " new_chromosome.ephemeral_random_constants = chromosome.ephemeral_random_constants\n", 761 | " return new_chromosome\n", 762 | "\n", 763 | " else:\n", 764 | "\n", 765 | " return chromosome\n", 766 | "\n", 767 | "\n", 768 | " @staticmethod\n", 769 | " def one_point_recombination(chromosome1: Chromosome, chromosome2: Chromosome) -> (Chromosome, Chromosome):\n", 770 | " \"\"\"\n", 771 | " Classical one point recombination.\n", 772 | "\n", 773 | " :param chromosome1: parent 1\n", 774 | " :param chromosome2: parent 2\n", 775 | " :return: offspring 1, offspring 2\n", 776 | " \"\"\"\n", 777 | " gene = randint(0, Chromosome.num_genes - 1)\n", 778 | " position = randint(0, Chromosome.length)\n", 779 | "\n", 780 | " child1_split_gene = chromosome1.genes[gene][:position] + chromosome2.genes[gene][position:]\n", 781 | " child2_split_gene = chromosome2.genes[gene][:position] + chromosome1.genes[gene][position:]\n", 782 | "\n", 783 | " child1_genes = chromosome1.genes[:gene] + [child1_split_gene] + (chromosome2.genes[gene+1:] if gene < Chromosome.num_genes - 1 else [])\n", 784 | " child2_genes = chromosome2.genes[:gene] + [child2_split_gene] + (chromosome1.genes[gene + 1:] if gene < Chromosome.num_genes - 1 else [])\n", 785 | "\n", 786 | " child1, child2 = Chromosome(child1_genes), Chromosome(child2_genes)\n", 787 | "\n", 788 | " constants_split_position = randint(0, Chromosome.length - 1)\n", 789 | " child1.ephemeral_random_constants = chromosome1.ephemeral_random_constants[:constants_split_position] + \\\n", 790 | " chromosome2.ephemeral_random_constants[constants_split_position:]\n", 791 | "\n", 792 | " child2.ephemeral_random_constants = chromosome2.ephemeral_random_constants[:constants_split_position] + \\\n", 793 | " chromosome1.ephemeral_random_constants[constants_split_position:]\n", 794 | "\n", 795 | " return child1, child2\n", 796 | "\n", 797 | "\n", 798 | " @staticmethod\n", 799 | " def two_point_recombination(chromosome1: Chromosome, chromosome2: Chromosome) -> (Chromosome, Chromosome):\n", 800 | " \"\"\"\n", 801 | " Classical two point recombination.\n", 802 | "\n", 803 | " :param chromosome1: parent 1\n", 804 | " :param chromosome2: parent 2\n", 805 | " :return: offspring 1, offsprint 2\n", 806 | " \"\"\"\n", 807 | "\n", 808 | " # generate crossover points\n", 809 | " position1, position2 = sorted([randint(0, Chromosome.length*Chromosome.num_genes - 1), randint(0, Chromosome.length*Chromosome.num_genes - 1)])\n", 810 | "\n", 811 | " # join genes into single string for ease of manipulation\n", 812 | " child1_genes_str = \"\".join(chromosome1.genes)\n", 813 | " child2_genes_str = \"\".join(chromosome2.genes)\n", 814 | "\n", 815 | " # perform crossover\n", 816 | " child1_genes = child1_genes_str[:position1] + child2_genes_str[position1:position2] + child1_genes_str[position2:]\n", 817 | " child2_genes = child2_genes_str[:position1] + child1_genes_str[position1:position2] + child2_genes_str[position2:]\n", 818 | "\n", 819 | " # split genes from string into list\n", 820 | " child1_genes = [child1_genes[i:i + Chromosome.length] for i in range(0, Chromosome.num_genes * Chromosome.length, Chromosome.length)]\n", 821 | " child2_genes = [child2_genes[i:i + Chromosome.length] for i in range(0, Chromosome.num_genes * Chromosome.length, Chromosome.length)]\n", 822 | "\n", 823 | " child1, child2 = Chromosome(child1_genes), Chromosome(child2_genes)\n", 824 | " split_positions = sorted([randint(0, Chromosome.length - 1), randint(0, Chromosome.length - 1)])\n", 825 | "\n", 826 | " child1.ephemeral_random_constants = chromosome1.ephemeral_random_constants[:split_positions[0]] + \\\n", 827 | " chromosome2.ephemeral_random_constants[split_positions[0]:split_positions[1]] + \\\n", 828 | " chromosome1.ephemeral_random_constants[split_positions[1]:]\n", 829 | " child2.ephemeral_random_constants = chromosome2.ephemeral_random_constants[:split_positions[0]] + \\\n", 830 | " chromosome1.ephemeral_random_constants[split_positions[0]:split_positions[1]] + \\\n", 831 | " chromosome2.ephemeral_random_constants[split_positions[1]:]\n", 832 | " return child1, child2\n", 833 | "\n", 834 | "\n", 835 | " @staticmethod\n", 836 | " def gene_recombination(chromosome1: Chromosome, chromosome2: Chromosome) -> (Chromosome, Chromosome):\n", 837 | " \"\"\"\n", 838 | " Two point recombination that occurs along gene boundaries for multigenic chromosomes.\n", 839 | "\n", 840 | " :param chromosome1: parent 1\n", 841 | " :param chromosome2: parent 2\n", 842 | " :return: offspring 1, offspring 2\n", 843 | " \"\"\"\n", 844 | "\n", 845 | " # choose gene to swap\n", 846 | " gene = randint(0, Chromosome.num_genes - 1)\n", 847 | "\n", 848 | " # initialize children genes\n", 849 | " child1_genes = chromosome1.genes\n", 850 | " child2_genes = chromosome2.genes\n", 851 | "\n", 852 | " # perform swap\n", 853 | " child1_genes[gene] = chromosome2.genes[gene]\n", 854 | " child2_genes[gene] = chromosome1.genes[gene]\n", 855 | "\n", 856 | " return Chromosome(child1_genes), Chromosome(child2_genes)\n", 857 | "\n", 858 | "\n", 859 | " @staticmethod\n", 860 | " def plot_reps(avg_fitnesses: list, best_fitnesses: list, random_search_avg: list = None, random_search_best: list = None) -> None:\n", 861 | " \"\"\"\n", 862 | " Plot all reps with global best solutions.\n", 863 | "\n", 864 | " :param avg_fitnesses: list of lists containing average fitnesses by generation for each rep\n", 865 | " :param best_fitnesses: same as avg_fitnesses but best fitnesses\n", 866 | " :param random_search_avg: average fitness value for random search across generations\n", 867 | " :param random_search_best: best fitness value for random search by generation\n", 868 | " :return: void\n", 869 | " \"\"\"\n", 870 | "\n", 871 | " is_random_search = not (random_search_avg is None or random_search_best is None)\n", 872 | "\n", 873 | " plt.subplots(1, 2, figsize=(16, 8))\n", 874 | "\n", 875 | " plt.subplot(1, 2, 1)\n", 876 | " plt.title(\"Average Fitness by Generation\")\n", 877 | " plt.xlabel(\"Generation\")\n", 878 | " plt.ylabel(\"Average Fitness\")\n", 879 | "\n", 880 | " # plot each rep\n", 881 | " for rep in range(GeneExpressionProgram.NUM_RUNS):\n", 882 | " plt.plot(range(len(avg_fitnesses[rep])), avg_fitnesses[rep], label=\"Rep %d Average\" % (rep + 1))\n", 883 | "\n", 884 | " if is_random_search:\n", 885 | " plt.plot(range(len(random_search_avg)), random_search_avg, label=\"Random Search Average\")\n", 886 | " #plt.plot(range(len(random_search_best)), random_search_best, label=\"Random Search Best\")\n", 887 | "\n", 888 | " plt.legend(loc=\"upper left\")\n", 889 | "\n", 890 | " plt.subplot(1, 2, 2)\n", 891 | " plt.title(\"Best Fitness by Generation\")\n", 892 | " plt.xlabel(\"Generation\")\n", 893 | " plt.ylabel(\"Best Fitness\")\n", 894 | "\n", 895 | " # plot each rep\n", 896 | " for rep in range(GeneExpressionProgram.NUM_RUNS):\n", 897 | " plt.plot(range(len(best_fitnesses[rep])), best_fitnesses[rep], label=\"Rep %d Best\" % (rep + 1))\n", 898 | "\n", 899 | " if is_random_search:\n", 900 | " plt.plot(range(len(random_search_best)), random_search_best, label=\"Random Search Best\")\n", 901 | "\n", 902 | " plt.legend(loc=\"upper left\")\n", 903 | " plt.show()\n" 904 | ] 905 | }, 906 | { 907 | "cell_type": "markdown", 908 | "metadata": {}, 909 | "source": [ 910 | "## Symbolic Regression: $f(a) = a^4 + a^3 + a^2 + a$\n", 911 | "Run this function to have GEP attempt to discover the function\n", 912 | "$$ f(a) = a^4 + a^3 + a^2 + a $$\n", 913 | "across 10 fitness cases. The fitness cases are of the form $\\left(a, f(a)\\right)$ for randomly generated $a$ between `OBJECTIVE_MIN` and `OBJECTIVE_MAX`. " 914 | ] 915 | }, 916 | { 917 | "cell_type": "code", 918 | "execution_count": null, 919 | "metadata": { 920 | "collapsed": true, 921 | "scrolled": true 922 | }, 923 | "outputs": [], 924 | "source": [ 925 | "def a4_a3_a2_a1():\n", 926 | "\n", 927 | " # define objective function\n", 928 | " GeneExpressionProgram.OBJECTIVE_FUNCTION = staticmethod(lambda a: a ** 4 + a ** 3 + a ** 2 + a)\n", 929 | " GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX = 0, 20\n", 930 | "\n", 931 | " # Define terminals and functions\n", 932 | " Chromosome.terminals = [\"a\"]\n", 933 | " Chromosome.functions = {\n", 934 | " \"+\": {\"args\": 2, \"f\": lambda x, y: x + y},\n", 935 | " \"-\": {\"args\": 2, \"f\": lambda x, y: x - y},\n", 936 | " \"*\": {\"args\": 2, \"f\": lambda x, y: x * y},\n", 937 | " \"/\": {\"args\": 2, \"f\": lambda x, y: x / y}\n", 938 | " }\n", 939 | " Chromosome.fitness_cases = [\n", 940 | " ({\"a\": a}, GeneExpressionProgram.OBJECTIVE_FUNCTION(a))\n", 941 | " for a in np.random.rand(GeneExpressionProgram.NUM_FITNESS_CASES) * (\n", 942 | " GeneExpressionProgram.OBJECTIVE_MAX - GeneExpressionProgram.OBJECTIVE_MIN) + GeneExpressionProgram.OBJECTIVE_MIN\n", 943 | " ]\n", 944 | "\n", 945 | " GeneExpressionProgram.FUNCTION_Y_RANGE = \\\n", 946 | " GeneExpressionProgram.OBJECTIVE_FUNCTION(GeneExpressionProgram.OBJECTIVE_MAX) - \\\n", 947 | " GeneExpressionProgram.OBJECTIVE_FUNCTION(GeneExpressionProgram.OBJECTIVE_MIN)\n", 948 | "\n", 949 | " Chromosome.linking_function = \"+\"\n", 950 | " Chromosome.max_fitness = 1 #GeneExpressionProgram.FUNCTION_Y_RANGE * GeneExpressionProgram.NUM_FITNESS_CASES\n", 951 | "\n", 952 | " ans = GeneExpressionProgram.evolve()\n", 953 | " ans.print_tree()" 954 | ] 955 | }, 956 | { 957 | "cell_type": "markdown", 958 | "metadata": {}, 959 | "source": [ 960 | "## Symbolic Regression: $f(x) = \\sin(x)$\n", 961 | "Watch as GEP attempts to approximate $f(x) = sin(x)$ using only addition, subtraction, multiplication, division, the variable $x$, and the number 6! If you're lucky, it may come up with the second order Taylor approximation centered at $x=0$:\n", 962 | "$$\\sin(x) \\approx x - \\frac{x^3}{6}$$\n", 963 | "However, it often finds the first order Taylor approximation, calls it good enough, and quits. The first order Taylor approximation is\n", 964 | "$$\\sin(x) \\approx x$$\n", 965 | "for $x \\approx 0$." 966 | ] 967 | }, 968 | { 969 | "cell_type": "code", 970 | "execution_count": null, 971 | "metadata": { 972 | "collapsed": true, 973 | "scrolled": true 974 | }, 975 | "outputs": [], 976 | "source": [ 977 | "def sinx_polynomial():\n", 978 | " # define objective function\n", 979 | " GeneExpressionProgram.OBJECTIVE_FUNCTION = staticmethod(lambda a: np.sin(a))\n", 980 | " GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX = -5, 5\n", 981 | " GeneExpressionProgram.FITNESS_FUNCTION = Chromosome.centralized_inv_squared_error\n", 982 | " GeneExpressionProgram.FITNESS_FUNCTION_ARGS = [0, \"x\"]\n", 983 | " GeneExpressionProgram.NUM_FITNESS_CASES = 10\n", 984 | " GeneExpressionProgram.NUM_GENERATIONS = 150\n", 985 | " GeneExpressionProgram.POPULATION_SIZE = 200\n", 986 | " GeneExpressionProgram.MUTATION_RATE = 0.05\n", 987 | "\n", 988 | " Chromosome.head_length = 8\n", 989 | " Chromosome.num_genes = 3\n", 990 | " Chromosome.length = 24\n", 991 | "\n", 992 | " # Define terminals and functions\n", 993 | " Chromosome.terminals = [\"x\", \"6\", \"!\"]\n", 994 | " Chromosome.constants = {\"!\": 120}\n", 995 | " Chromosome.functions = {\n", 996 | " \"+\": {\"args\": 2, \"f\": lambda x, y: x + y},\n", 997 | " \"-\": {\"args\": 2, \"f\": lambda x, y: x - y},\n", 998 | " \"*\": {\"args\": 2, \"f\": lambda x, y: x * y},\n", 999 | " \"/\": {\"args\": 2, \"f\": lambda x, y: x / y}\n", 1000 | " #\"^\": {\"args\": 2, \"f\": lambda x, y: x ** y}\n", 1001 | " }\n", 1002 | " Chromosome.fitness_cases = [\n", 1003 | " ({\"x\": x}, GeneExpressionProgram.OBJECTIVE_FUNCTION(x))\n", 1004 | " for x in np.random.rand(GeneExpressionProgram.NUM_FITNESS_CASES) * (\n", 1005 | " GeneExpressionProgram.OBJECTIVE_MAX - GeneExpressionProgram.OBJECTIVE_MIN) + GeneExpressionProgram.OBJECTIVE_MIN\n", 1006 | " ]\n", 1007 | "\n", 1008 | " GeneExpressionProgram.FUNCTION_Y_RANGE = 2\n", 1009 | " Chromosome.linking_function = \"*\"\n", 1010 | " Chromosome.max_fitness = 1 #GeneExpressionProgram.FUNCTION_Y_RANGE * GeneExpressionProgram.NUM_FITNESS_CASES\n", 1011 | "\n", 1012 | " ans, average_fitnesses, best_fitnesses = GeneExpressionProgram.evolve()\n", 1013 | " ans.print_tree()\n", 1014 | " ans.plot_solution(GeneExpressionProgram.OBJECTIVE_FUNCTION,\n", 1015 | " GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX,\n", 1016 | " average_fitnesses, best_fitnesses)" 1017 | ] 1018 | }, 1019 | { 1020 | "cell_type": "markdown", 1021 | "metadata": {}, 1022 | "source": [ 1023 | "## CartPole v1\n", 1024 | "GEP evolves a real-valued function which is positive when the cart should move to the right and negative when the cart should move to the left." 1025 | ] 1026 | }, 1027 | { 1028 | "cell_type": "code", 1029 | "execution_count": null, 1030 | "metadata": { 1031 | "collapsed": true, 1032 | "scrolled": true 1033 | }, 1034 | "outputs": [], 1035 | "source": [ 1036 | "def cart_pole_real():\n", 1037 | "\n", 1038 | " import gym\n", 1039 | " env = gym.make('CartPole-v1')\n", 1040 | "\n", 1041 | " def fitness(num_trials: int, render: bool = False, doPrint = False, *args) -> np.ndarray:\n", 1042 | " fitnesses = []\n", 1043 | " for chromosome_index in range(len(args)):\n", 1044 | " chromosome = args[chromosome_index]\n", 1045 | " total_reward = 0\n", 1046 | " if doPrint:\n", 1047 | " print(\"\\tCalculating chromosome %d across %d trials.\" % (chromosome_index, num_trials), end=\"\\t\")\n", 1048 | " for trial in range(num_trials):\n", 1049 | " x, theta, dx, dtheta = env.reset()\n", 1050 | " t = 0\n", 1051 | " while True:\n", 1052 | " if render: env.render()\n", 1053 | " action = chromosome.evaluate({\"x\": x, \"v\": dx, \"t\": theta, \"u\": dtheta})\n", 1054 | " observation, reward, done, info = env.step(action > 0)\n", 1055 | " x, theta, dx, dtheta = observation\n", 1056 | " total_reward += reward\n", 1057 | " if done:\n", 1058 | " break\n", 1059 | " t += 1\n", 1060 | " chromosome._fitness_ = total_reward / float(num_trials)\n", 1061 | " fitnesses.append(total_reward / float(num_trials))\n", 1062 | " if doPrint:\n", 1063 | " print(\"Fitness: %.5f\" % (total_reward / float(num_trials)))\n", 1064 | " return np.asarray(fitnesses)\n", 1065 | "\n", 1066 | " Chromosome.functions = {\n", 1067 | " \"+\": {\"args\": 2, \"f\": lambda x, y: x + y},\n", 1068 | " \"-\": {\"args\": 2, \"f\": lambda x, y: x - y},\n", 1069 | " \"*\": {\"args\": 2, \"f\": lambda x, y: x * y},\n", 1070 | " \"/\": {\"args\": 2, \"f\": lambda x, y: x / y}\n", 1071 | " }\n", 1072 | "\n", 1073 | " Chromosome.terminals = [\n", 1074 | " \"x\", # cart x-value\n", 1075 | " \"v\", # cart velocity\n", 1076 | " \"t\", # pole angle\n", 1077 | " \"u\" # rate of change of pole angle\n", 1078 | " ]\n", 1079 | "\n", 1080 | " GeneExpressionProgram.FITNESS_FUNCTION = fitness\n", 1081 | " GeneExpressionProgram.FITNESS_FUNCTION_ARGS = [10, False, False]\n", 1082 | " GeneExpressionProgram.POPULATION_SIZE = 10\n", 1083 | " GeneExpressionProgram.ERROR_TOLERANCE = 0\n", 1084 | " GeneExpressionProgram.NUM_RUNS = 5\n", 1085 | " GeneExpressionProgram.NUM_GENERATIONS = 25\n", 1086 | "\n", 1087 | " Chromosome.max_fitness = 500\n", 1088 | " Chromosome.num_genes = 3\n", 1089 | " Chromosome.length = 30\n", 1090 | " Chromosome.head_length = 10\n", 1091 | " Chromosome.linking_function = \"+\"\n", 1092 | "\n", 1093 | " average_fitnesses = []\n", 1094 | " best_fitnesses = []\n", 1095 | "\n", 1096 | " for rep in range(GeneExpressionProgram.NUM_RUNS):\n", 1097 | " ans, gen_average_fitnesses, gen_best_fitnesses = GeneExpressionProgram.evolve()\n", 1098 | " average_fitnesses.append(gen_average_fitnesses)\n", 1099 | " best_fitnesses.append(gen_best_fitnesses)\n", 1100 | " ans.print_tree()\n", 1101 | " print(ans.genes)\n", 1102 | "\n", 1103 | " if fitness(100, False, True, ans)[0] == Chromosome.max_fitness:\n", 1104 | " fitness(5, True, False, ans)\n", 1105 | " \n", 1106 | "\n", 1107 | " GeneExpressionProgram.plot_reps(average_fitnesses, best_fitnesses)\n", 1108 | "\n", 1109 | " # Very good genes (500 fitness over 100 attempts):\n", 1110 | " # ['-u-uvx*txtxxutuutvtvtvttttutvu', '+*vxu/x/-/tutxtxxxuvuuxvtuvvtu', '+ut*vv*vtvxvxvuxvtxvtvuutvtttt']\n", 1111 | " # ['t/v*/xtttvuxtvvttutxxtuvxuvttt', '-v*+v*+t+tuxtvvttuuvxvtuvuvxtt', 'u++tu+uvtvtxtvtvtuvxxttvxvxvv']\n", 1112 | " # ['-t-*+u*u-*tuutxxuutxtxttxttvxu', '+-u**+-/vxvxxxvtxvxuxvxvvxxtvx', '+xv-xvx-xtuutxxtxxxtxxuttvxxut']" 1113 | ] 1114 | }, 1115 | { 1116 | "cell_type": "markdown", 1117 | "metadata": { 1118 | "collapsed": true 1119 | }, 1120 | "source": [ 1121 | "## Hard Symbolic Regression\n", 1122 | "GEP attempts to find the equation\n", 1123 | "$$ f(a) = 4.251a^2 + \\ln(a^2) + 7.243e^a $$\n", 1124 | "using ephemeral random constants across 5000 generations and 5 reps. This often gets stuck in local optima, and hence does not always attain a large fitness value. The success of this algorithm depends greatly on the luck of the initial population and the probability of favorable mutations." 1125 | ] 1126 | }, 1127 | { 1128 | "cell_type": "code", 1129 | "execution_count": null, 1130 | "metadata": { 1131 | "collapsed": true 1132 | }, 1133 | "outputs": [], 1134 | "source": [ 1135 | "def hard_regression():\n", 1136 | " # TODO - crossover and mutation of constants\n", 1137 | " # define objective function\n", 1138 | " GeneExpressionProgram.OBJECTIVE_FUNCTION = staticmethod(lambda a: 4.251*np.power(a, 2) + np.log(np.power(a, 2)) + 7.243*np.exp(a))\n", 1139 | " GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX = -1, 1\n", 1140 | " GeneExpressionProgram.NUM_FITNESS_CASES = 20\n", 1141 | " GeneExpressionProgram.FITNESS_FUNCTION = Chromosome.inv_squared_error\n", 1142 | " GeneExpressionProgram.NUM_GENERATIONS = 5000\n", 1143 | " GeneExpressionProgram.NUM_RUNS = 5\n", 1144 | " GeneExpressionProgram.MUTATION_RATE = 0.041\n", 1145 | "\n", 1146 | " # Define terminals and functions\n", 1147 | " Chromosome.terminals = [\"a\", \"?\"]\n", 1148 | " Chromosome.ephemeral_random_constants_range = (-1, 1)\n", 1149 | " Chromosome.functions = {\n", 1150 | " \"+\": {\"args\": 2, \"f\": lambda x, y: x + y},\n", 1151 | " \"-\": {\"args\": 2, \"f\": lambda x, y: x - y},\n", 1152 | " \"*\": {\"args\": 2, \"f\": lambda x, y: x * y},\n", 1153 | " \"/\": {\"args\": 2, \"f\": lambda x, y: x / y if y != 0 else np.nan},\n", 1154 | " \"L\": {\"args\": 1, \"f\": lambda x: np.log(x) if x > 0 else np.nan},\n", 1155 | " \"E\": {\"args\": 1, \"f\": lambda x: np.exp(x)},\n", 1156 | " #\"K\": {\"args\": 1, \"f\": lambda x: np.log10(x) if x > 0 else np.nan},\n", 1157 | " #\"~\": {\"args\": 1, \"f\": lambda x: np.power(10, x)},\n", 1158 | " #\"S\": {\"args\": 1, \"f\": lambda x: np.sin(x)},\n", 1159 | " #\"C\": {\"args\": 1, \"f\": lambda x: np.cos(x)}\n", 1160 | " }\n", 1161 | " Chromosome.fitness_cases = [\n", 1162 | " ({\"a\": a}, GeneExpressionProgram.OBJECTIVE_FUNCTION(a))\n", 1163 | " for a in np.random.rand(GeneExpressionProgram.NUM_FITNESS_CASES) * (\n", 1164 | " GeneExpressionProgram.OBJECTIVE_MAX - GeneExpressionProgram.OBJECTIVE_MIN) + GeneExpressionProgram.OBJECTIVE_MIN\n", 1165 | " ]\n", 1166 | "\n", 1167 | " Chromosome.linking_function = \"+\"\n", 1168 | " Chromosome.length = 60\n", 1169 | " Chromosome.head_length = 20\n", 1170 | " Chromosome.max_fitness = 1 #GeneExpressionProgram.FUNCTION_Y_RANGE * GeneExpressionProgram.NUM_FITNESS_CASES\n", 1171 | "\n", 1172 | " average_fitnesses = []\n", 1173 | " best_fitnesses = []\n", 1174 | " answers = []\n", 1175 | "\n", 1176 | " ans = None\n", 1177 | " for rep in range(GeneExpressionProgram.NUM_RUNS):\n", 1178 | " print(\"====================================== Rep %d ======================================\" % rep)\n", 1179 | " ans, gen_average_fitnesses, gen_best_fitnesses = GeneExpressionProgram.evolve()\n", 1180 | " average_fitnesses.append(gen_average_fitnesses)\n", 1181 | " best_fitnesses.append(gen_best_fitnesses)\n", 1182 | " ans.print_tree()\n", 1183 | " print(ans.genes)\n", 1184 | " answers.append(deepcopy(ans))\n", 1185 | "\n", 1186 | " print(\"====================================== Random Search ======================================\")\n", 1187 | " random_search_individual, random_search_avg, random_search_best = GeneExpressionProgram.random_search(\n", 1188 | " GeneExpressionProgram.NUM_GENERATIONS,\n", 1189 | " GeneExpressionProgram.FITNESS_FUNCTION,\n", 1190 | " GeneExpressionProgram.FITNESS_FUNCTION_ARGS\n", 1191 | " )\n", 1192 | "\n", 1193 | " # Good genes:\n", 1194 | " #ans = Chromosome(['+/aE?aa?aaEa*Ea+E/?L?aa??aaaaa???aaaaa???aa?a??a?aa???aaa??a?a??a?aaaaaa?aaa??aa', '?a???L*Laa+++L-?///aaa??????a?aa?aaa?aaa?aaa?aa??a?a?a?aa??aaa?a?aaaaaa?aa???aaa', '+L**E*aaEaa+E??*/a*-???a?aa?aaaa?aaaaaaa?a?aaaa?aa?aaaaaaa???aa?aaaa?aa??aaa????'])\n", 1195 | " #ans.ephemeral_random_constants = [0.15711028370174618, 0.8582913293794245, 0.08834476189713181, -0.6507871955790874, -0.5001482762755984, -0.36339831251011967, 0.49239731282122023, -0.7707384044151064, 0.33957437653782097, -0.050909069821311936, 0.40688394042469067, 0.8838615430620207, -0.03950637280673064, -0.8398663459127116, -0.9701111175669401, 0.16516078130630563, -0.5163031755060181, 0.26930803528455916, -0.7833159749333989, -0.7075160969083776, -0.6751546227334948, 0.1505636368911023, -0.16805240822390255, -0.34424168190370485, -0.8544980338428079, -0.01217703484745547, -0.24005751860391533, -0.5077198074421936, -0.47443544577074226, 0.5247967085947582, -0.22543318048008576, 0.4938865002308659, -0.6465093618701077, -0.19098460727467326, -0.2944062401077585, 0.7016839377380519, -0.14341637591100542, 0.23227088210671476, 0.36051772215302, 0.6509343605188611, -0.332150327502315, 0.3171544096208032, 0.2676850827993431, -0.46506262502073414, 0.843996276413379, -0.5614960005334659, 0.47891344757490373, -0.5575325624206526, -0.8156525364269045, -0.31652263727243746, 0.06884531540832572, 0.8836222097723032, 0.6601557412383419, -0.7869161076217597, -0.16110865846423783, -0.06702662866877018, -0.22568995470545228, 0.9438977429740325, -0.6410133183089668, 0.045353882523733624, 0.16786587502585948, -0.9008872395302681, 0.004355424045595413, 0.9179463218466064, 0.4105708444235643, -0.001799675111191501, -0.4201794697378056, -0.37672122028632216, 0.5906938113771141, 0.6004718433032417, -0.4698494772153414, 0.04238505345795085, 0.1657146428146341, 0.5148585050576182, 0.6955837854892111, -0.08812485635975653, -0.4101965485774235, 0.7234363214796748, 0.14285945798729927, 0.6450352601522822]\n", 1196 | " #ans.plot_solution(GeneExpressionProgram.OBJECTIVE_FUNCTION, GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX, [], [], \"a\")\n", 1197 | "\n", 1198 | " #ans = Chromosome([0.10325095527275208, -0.6353207023355358, 0.3087002846077995, -0.646063772861873, -0.7604999404652737, -0.7026365243175745, -0.8546384172276964, -0.9210656698793773, -0.92473371383499159, 0.38475054329790037, 0.011392225394319944, -0.18840805047095377, 0.40103873272978197, 0.06328742279657229, 0.7176441901359412, -0.26941389839428265, 0.8330939983953638, -0.3291581824265808, -0.6081511326274629, 0.9437496769675557, 0.14649019501924365, -0.026586568971478375, 0.6942711369955727, -0.4952751886918654, 0.49348107956565457, 0.6373366861063929, -0.4180513467203788, 0.37222546291909575, 0.09407582333744235, -0.7976697480943422, -0.04875126785507633, 0.0738340898048051, 0.4574594498229325, 0.6176526203069368, -0.8316660105318336, 0.328772037314927, 0.854958551073552, -0.3935592840377773, -0.1523385500807699, -0.8272148377421589, 0.8580710774374207, -0.18672644199209687, 0.5393722078778671, -0.6906345459192962, -0.22443846331305117, 0.34512085908760115, 0.21516058072431332, -0.8499353974445536, 0.26989917691422316, -0.44201601204751584, 0.00887253706744362, -0.7836088648008288, -0.8679234491746306, -0.07209104779770104, 0.16000238129327515, 0.8890857194001152, -0.6325595149575627, -0.11804832230841544, -0.018168330413408817, -0.6561487673112327, 0.18667288624065526, -0.21171583904469493, -0.7074160949096726, -0.5647179047402893, 0.6466522462832285, 0.17157152187819658, -0.55051406791476754, -0.32459344245456756, -0.73568042741430184, 0.39543504084587644, -0.9332865781958235, 0.9903268434472272, 0.05041449572068979, 0.0798497529910911, -0.66617358746171074, 0.5071192429292173, 0.38014218205802264, -0.929868126429702, -0.23615736970440504, 0.5939936181262968])\n", 1199 | " #ans.ephemeral_random_constants = ['+EEEaa/L-+L-EL-L?aLEaa??????aa?aa?????aa??a???aaaaaaa????aa?aa??aa??a?a??aaa?aaa', '+EEE*aa?+?*/LLa/*+*-a?a?aaaa??a?aa?a??aa???aaaaa????a???????a?aaa????????aaaaaa?', '-L***aaa--+*+*+?+?--?aa??aa?aa?aa??a??a?aa??aaaaa?a??a?aa?aa?????aaaa??aa?aaa?a?']\n", 1200 | "\n", 1201 | " best = max(answers, key=lambda c:c.fitness())\n", 1202 | " ans.plot_solution(GeneExpressionProgram.OBJECTIVE_FUNCTION, GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX, average_fitnesses[0], best_fitnesses[0], \"a\")\n", 1203 | " GeneExpressionProgram.plot_reps(average_fitnesses, best_fitnesses, random_search_avg, random_search_best)" 1204 | ] 1205 | }, 1206 | { 1207 | "cell_type": "code", 1208 | "execution_count": null, 1209 | "metadata": {}, 1210 | "outputs": [], 1211 | "source": [ 1212 | "cart_pole_real()" 1213 | ] 1214 | }, 1215 | { 1216 | "cell_type": "code", 1217 | "execution_count": null, 1218 | "metadata": { 1219 | "collapsed": true 1220 | }, 1221 | "outputs": [], 1222 | "source": [] 1223 | } 1224 | ], 1225 | "metadata": { 1226 | "kernelspec": { 1227 | "display_name": "Python 3", 1228 | "language": "python", 1229 | "name": "python3" 1230 | }, 1231 | "language_info": { 1232 | "codemirror_mode": { 1233 | "name": "ipython", 1234 | "version": 3 1235 | }, 1236 | "file_extension": ".py", 1237 | "mimetype": "text/x-python", 1238 | "name": "python", 1239 | "nbconvert_exporter": "python", 1240 | "pygments_lexer": "ipython3", 1241 | "version": "3.6.1" 1242 | } 1243 | }, 1244 | "nbformat": 4, 1245 | "nbformat_minor": 2 1246 | } 1247 | -------------------------------------------------------------------------------- /GeneExpressionProgram.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | from random import random, randint, shuffle 5 | 6 | from Chromosome import Chromosome 7 | 8 | class GeneExpressionProgram: 9 | 10 | ### Hyperparameters ### 11 | NUM_RUNS = 5 12 | NUM_GENERATIONS = 500 13 | POPULATION_SIZE = 100 14 | NUM_FITNESS_CASES = 10 15 | ERROR_TOLERANCE = 0.0000001 16 | 17 | ### Reproduction ### 18 | MUTATION_RATE = 0.051 19 | ONE_POINT_CROSSOVER_RATE, TWO_POINT_CROSSOVER_RATE, GENE_CROSSOVER_RATE = 0.2, 0.5, 0.1 20 | IS_TRANSPOSITION_RATE, IS_ELEMENTS_LENGTH = 0.1, [1, 2, 3] 21 | RIS_TRANSPOSITION_RATE, RIS_ELEMENTS_LENGTH = 0.1, [1, 2, 3] 22 | GENE_TRANSPOSITION_RATE = 0.1 23 | 24 | ### Fitness Evaluation ### 25 | OBJECTIVE_FUNCTION = None 26 | FITNESS_FUNCTION = None 27 | FITNESS_FUNCTION_ARGS = list() 28 | OBJECTIVE_MIN, OBJECTIVE_MAX = None, None 29 | FUNCTION_Y_RANGE = None 30 | 31 | 32 | def __init__(self): 33 | pass 34 | 35 | 36 | @staticmethod 37 | def evolve() -> (Chromosome, list, list): 38 | """ 39 | Execute Gene Expression Programming algorithm 40 | 41 | :return: tuple of: 42 | the best fit chromosome, 43 | list of average fitness by generation (for plotting), 44 | list of best fitness by generation (for plotting 45 | """ 46 | 47 | # create initial population 48 | population = [Chromosome.generate_random_individual() for _ in range(GeneExpressionProgram.POPULATION_SIZE)] 49 | 50 | generation = 0 51 | best_fit_individual = None 52 | average_fitness_by_generation = [] 53 | best_fitness_by_generation = [] 54 | while generation < GeneExpressionProgram.NUM_GENERATIONS: 55 | 56 | ### EVALUATION ### 57 | 58 | # calcluate fitnesses for population 59 | population_fitnesses = GeneExpressionProgram.FITNESS_FUNCTION(*GeneExpressionProgram.FITNESS_FUNCTION_ARGS, *population) 60 | 61 | # find best fit individual 62 | # noinspection PyTypeChecker 63 | best_fit_generation = population[np.argmax(population_fitnesses)] 64 | if generation == 0 or best_fit_individual.fitness() < best_fit_generation.fitness(): 65 | best_fit_individual = deepcopy(best_fit_generation) 66 | 67 | # skip rest of loop if we have found optimal solution 68 | if abs(best_fit_individual.fitness() - Chromosome.max_fitness) <= GeneExpressionProgram.ERROR_TOLERANCE: 69 | average_fitness_generation = float(np.mean(population_fitnesses)) 70 | average_fitness_by_generation.append(average_fitness_generation) 71 | best_fitness_by_generation.append(best_fit_individual.fitness()) 72 | break 73 | 74 | next_generation = list() 75 | 76 | 77 | ### SELECTION (roulette wheel with simple elitism) ### 78 | 79 | # copy best individual to next generation 80 | next_generation.append(deepcopy(best_fit_individual)) 81 | 82 | # select the rest of the next generation with roulette wheel selection 83 | all_parents = GeneExpressionProgram.roulette_wheel_selection(population, len(population)) 84 | 85 | # Mutation 86 | all_parents = list(map(GeneExpressionProgram.mutate, all_parents)) 87 | 88 | # IS Transposition 89 | all_parents = list(map(GeneExpressionProgram.is_transposition, all_parents)) 90 | 91 | # RIS Transposition 92 | all_parents = list(map(GeneExpressionProgram.ris_transposition, all_parents)) 93 | 94 | # Gene Transposition 95 | all_parents = list(map(GeneExpressionProgram.gene_transposition, all_parents)) 96 | 97 | # Recombination 98 | shuffle(all_parents) 99 | for i in range(1, GeneExpressionProgram.POPULATION_SIZE, 2): 100 | 101 | # in case we don't have a pair to check for crossover, avoid index error 102 | if i + 1 >= GeneExpressionProgram.POPULATION_SIZE: 103 | next_generation.append(all_parents[i]) 104 | break 105 | 106 | child1, child2 = all_parents[i], all_parents[i+1] 107 | 108 | # One-point Recombination 109 | if random() < GeneExpressionProgram.ONE_POINT_CROSSOVER_RATE: 110 | child1, child2 = GeneExpressionProgram.one_point_recombination(child1, child2) 111 | 112 | # Two-point Recombination 113 | elif random() < GeneExpressionProgram.TWO_POINT_CROSSOVER_RATE: 114 | child1, child2 = GeneExpressionProgram.two_point_recombination(child1, child2) 115 | 116 | # Gene Recombination 117 | elif random() < GeneExpressionProgram.GENE_CROSSOVER_RATE: 118 | child1, child2 = GeneExpressionProgram.gene_recombination(child1, child2) 119 | 120 | # Include children in next generation 121 | next_generation.append(child1) 122 | next_generation.append(child2) 123 | 124 | # prepare for next iteration 125 | population = next_generation 126 | generation += 1 127 | 128 | average_fitness_generation = float(np.mean(population_fitnesses)) 129 | average_fitness_by_generation.append(average_fitness_generation) 130 | best_fitness_by_generation.append(best_fit_individual.fitness()) 131 | print("Generation: %d\tPopulation Size: %d\tAverage Fitness: %.5f\tBest Fitness (overall): %.5f" % 132 | (generation, len(population), average_fitness_generation, best_fit_individual.fitness())) 133 | 134 | return best_fit_individual, average_fitness_by_generation, best_fitness_by_generation 135 | 136 | 137 | @staticmethod 138 | def random_search(num_generations: int, fitness_function: callable, fitness_function_args: list) -> (Chromosome, list, list): 139 | best = None 140 | best_fitness = 0 141 | average_fitnesses = [] 142 | best_fitnesses = [] 143 | for gen in range(num_generations): 144 | generation_fitnesses = [] 145 | for individual in range(GeneExpressionProgram.POPULATION_SIZE): 146 | current = Chromosome.generate_random_individual() 147 | current_fitness = fitness_function(*fitness_function_args, current) 148 | generation_fitnesses.append(current_fitness) 149 | if best is None or best_fitness <= current_fitness: 150 | best = deepcopy(current) 151 | best_fitness = best.fitness() 152 | average_fitnesses.append(np.mean(generation_fitnesses)) 153 | best_fitnesses.append(best_fitness) 154 | if best_fitness >= Chromosome.max_fitness: 155 | break 156 | return best, average_fitnesses, best_fitnesses 157 | 158 | 159 | @staticmethod 160 | def roulette_wheel_selection(chromosomes: list, n: int) -> list: 161 | """ 162 | Returns n randomly selected chromosomes using roulette wheel selection. 163 | Adapted from: 164 | https://stackoverflow.com/questions/2140787/select-k-random-elements-from-a-list-whose-elements-have-weights 165 | 166 | :param chromosomes: list of chromosomes to select from 167 | :param n: number of samples to retrieve 168 | :return: list of chosen chromosome(s) 169 | """ 170 | total = float(sum(c.fitness() for c in chromosomes)) 171 | i = 0 172 | fitness = chromosomes[0].fitness() 173 | while n: 174 | x = total * (1 - random() ** (1.0 / n)) 175 | total -= x 176 | while x > fitness: 177 | x -= fitness 178 | i += 1 179 | fitness = chromosomes[i].fitness() 180 | fitness -= x 181 | yield chromosomes[i] 182 | n -= 1 183 | 184 | 185 | @staticmethod 186 | def mutate(chromosome: Chromosome) -> Chromosome: 187 | """ 188 | Randomly mutate genes in a chromosome. 189 | 190 | :param chromosome: chromosome to mutate 191 | :return: mutated chromosome 192 | """ 193 | 194 | head_characters = list(Chromosome.functions.keys()) + Chromosome.terminals 195 | 196 | new_genes = [] 197 | # for each gene (multigenic chromosomes) 198 | for gene in range(Chromosome.num_genes): 199 | new_gene = "" 200 | # for each character in the gene 201 | for i in range(len(chromosome.genes[gene])): 202 | # if we are to mutate it 203 | if random() < GeneExpressionProgram.MUTATION_RATE: 204 | # if we are mutating the head 205 | if i < Chromosome.head_length: 206 | new_gene += head_characters[randint(0, len(head_characters) - 1)] 207 | else: 208 | new_gene += Chromosome.terminals[randint(0, len(Chromosome.terminals) - 1)] 209 | else: 210 | new_gene += chromosome.genes[gene][i] 211 | new_genes.append(new_gene) 212 | 213 | # create new chromosome to ensure memoized fitness values are recalculated. 214 | new_chromosome = Chromosome(new_genes) 215 | new_chromosome.ephemeral_random_constants = chromosome.ephemeral_random_constants 216 | 217 | # mutate ephemeral random constants 218 | for constant in range(len(new_chromosome.ephemeral_random_constants)): 219 | if random() < GeneExpressionProgram.MUTATION_RATE: 220 | new_chromosome.ephemeral_random_constants[constant] = np.random.uniform(*Chromosome.ephemeral_random_constants_range) 221 | 222 | return new_chromosome 223 | 224 | 225 | @staticmethod 226 | def is_transposition(chromosome: Chromosome) -> Chromosome: 227 | """ 228 | Insertion Sequence transposition. 229 | 230 | :param chromosome: chromosome to perform IS transposition on 231 | :return: new chromosome 232 | """ 233 | 234 | # TODO - properly transpose ephemeral random constants 235 | 236 | if random() < GeneExpressionProgram.IS_TRANSPOSITION_RATE: 237 | 238 | # determine parameters of transposition 239 | length = np.random.choice(GeneExpressionProgram.IS_ELEMENTS_LENGTH) 240 | source_gene = randint(0, len(chromosome.genes) - 1) 241 | target_gene = randint(0, len(chromosome.genes) - 1) 242 | target_position = randint(1, Chromosome.head_length - length) 243 | sequence_start = randint(0, len(chromosome.genes[source_gene])) 244 | 245 | transposition_string = chromosome.genes[source_gene][sequence_start:min(Chromosome.length, sequence_start + length)] 246 | 247 | # make substitution 248 | new_chromosome = Chromosome(chromosome.genes) 249 | new_chromosome.ephemeral_random_constants = chromosome.ephemeral_random_constants 250 | new_chromosome.genes[target_gene] = new_chromosome.genes[target_gene][:target_position] + \ 251 | transposition_string + \ 252 | new_chromosome.genes[target_gene][target_position + length:] 253 | 254 | return new_chromosome 255 | 256 | else: 257 | 258 | return chromosome 259 | 260 | 261 | @staticmethod 262 | def ris_transposition(chromosome: Chromosome) -> Chromosome: 263 | """ 264 | Root Insertion Sequence transposition. 265 | 266 | :param chromosome: chromosome to perform RIS transposition on 267 | :return: new chromosome 268 | """ 269 | 270 | # TODO - properly transpose ephemeral random constants 271 | 272 | start_point = randint(0, Chromosome.head_length - 1) 273 | gene = randint(0, Chromosome.num_genes - 1) 274 | while start_point < Chromosome.head_length and chromosome.genes[gene][start_point] not in Chromosome.functions: 275 | start_point += 1 276 | 277 | if random() < GeneExpressionProgram.RIS_TRANSPOSITION_RATE and chromosome.genes[gene][start_point] in Chromosome.functions: 278 | ris_length = np.random.choice(GeneExpressionProgram.RIS_ELEMENTS_LENGTH) 279 | ris_string = chromosome.genes[gene][start_point:start_point+ris_length] 280 | 281 | new_chromosome = Chromosome(chromosome.genes) 282 | new_chromosome.ephemeral_random_constants = chromosome.ephemeral_random_constants 283 | old_head = new_chromosome.genes[gene][:Chromosome.head_length] 284 | new_head = old_head[:start_point] + ris_string + old_head[start_point:] 285 | new_chromosome.genes[gene] = new_head[:Chromosome.head_length] + new_chromosome.genes[gene][Chromosome.head_length:] 286 | 287 | return new_chromosome 288 | 289 | else: 290 | 291 | return chromosome 292 | 293 | 294 | @staticmethod 295 | def gene_transposition(chromosome: Chromosome) -> Chromosome: 296 | """ 297 | Gene Insertion Sequence transposition. 298 | 299 | :param chromosome: chromosome to perform gene transposition on 300 | :return: new chromosome 301 | """ 302 | 303 | # TODO - properly transpose ephemeral random constants 304 | 305 | if Chromosome.num_genes > 1 and random() < GeneExpressionProgram.GENE_TRANSPOSITION_RATE: 306 | 307 | index = randint(0, Chromosome.num_genes - 1) 308 | temp = chromosome.genes[index] 309 | chromosome.genes[index] = chromosome.genes[0] 310 | chromosome.genes[0] = temp 311 | new_chromosome = Chromosome(chromosome.genes) 312 | new_chromosome.ephemeral_random_constants = chromosome.ephemeral_random_constants 313 | return new_chromosome 314 | 315 | else: 316 | 317 | return chromosome 318 | 319 | 320 | @staticmethod 321 | def one_point_recombination(chromosome1: Chromosome, chromosome2: Chromosome) -> (Chromosome, Chromosome): 322 | """ 323 | Classical one point recombination. 324 | 325 | :param chromosome1: parent 1 326 | :param chromosome2: parent 2 327 | :return: offspring 1, offspring 2 328 | """ 329 | gene = randint(0, Chromosome.num_genes - 1) 330 | position = randint(0, Chromosome.length) 331 | 332 | child1_split_gene = chromosome1.genes[gene][:position] + chromosome2.genes[gene][position:] 333 | child2_split_gene = chromosome2.genes[gene][:position] + chromosome1.genes[gene][position:] 334 | 335 | child1_genes = chromosome1.genes[:gene] + [child1_split_gene] + (chromosome2.genes[gene+1:] if gene < Chromosome.num_genes - 1 else []) 336 | child2_genes = chromosome2.genes[:gene] + [child2_split_gene] + (chromosome1.genes[gene + 1:] if gene < Chromosome.num_genes - 1 else []) 337 | 338 | child1, child2 = Chromosome(child1_genes), Chromosome(child2_genes) 339 | 340 | constants_split_position = randint(0, Chromosome.length - 1) 341 | child1.ephemeral_random_constants = chromosome1.ephemeral_random_constants[:constants_split_position] + \ 342 | chromosome2.ephemeral_random_constants[constants_split_position:] 343 | 344 | child2.ephemeral_random_constants = chromosome2.ephemeral_random_constants[:constants_split_position] + \ 345 | chromosome1.ephemeral_random_constants[constants_split_position:] 346 | 347 | return child1, child2 348 | 349 | 350 | @staticmethod 351 | def two_point_recombination(chromosome1: Chromosome, chromosome2: Chromosome) -> (Chromosome, Chromosome): 352 | """ 353 | Classical two point recombination. 354 | 355 | :param chromosome1: parent 1 356 | :param chromosome2: parent 2 357 | :return: offspring 1, offsprint 2 358 | """ 359 | 360 | # generate crossover points 361 | position1, position2 = sorted([randint(0, Chromosome.length*Chromosome.num_genes - 1), randint(0, Chromosome.length*Chromosome.num_genes - 1)]) 362 | 363 | # join genes into single string for ease of manipulation 364 | child1_genes_str = "".join(chromosome1.genes) 365 | child2_genes_str = "".join(chromosome2.genes) 366 | 367 | # perform crossover 368 | child1_genes = child1_genes_str[:position1] + child2_genes_str[position1:position2] + child1_genes_str[position2:] 369 | child2_genes = child2_genes_str[:position1] + child1_genes_str[position1:position2] + child2_genes_str[position2:] 370 | 371 | # split genes from string into list 372 | child1_genes = [child1_genes[i:i + Chromosome.length] for i in range(0, Chromosome.num_genes * Chromosome.length, Chromosome.length)] 373 | child2_genes = [child2_genes[i:i + Chromosome.length] for i in range(0, Chromosome.num_genes * Chromosome.length, Chromosome.length)] 374 | 375 | child1, child2 = Chromosome(child1_genes), Chromosome(child2_genes) 376 | split_positions = sorted([randint(0, Chromosome.length - 1), randint(0, Chromosome.length - 1)]) 377 | 378 | child1.ephemeral_random_constants = chromosome1.ephemeral_random_constants[:split_positions[0]] + \ 379 | chromosome2.ephemeral_random_constants[split_positions[0]:split_positions[1]] + \ 380 | chromosome1.ephemeral_random_constants[split_positions[1]:] 381 | child2.ephemeral_random_constants = chromosome2.ephemeral_random_constants[:split_positions[0]] + \ 382 | chromosome1.ephemeral_random_constants[split_positions[0]:split_positions[1]] + \ 383 | chromosome2.ephemeral_random_constants[split_positions[1]:] 384 | return child1, child2 385 | 386 | 387 | @staticmethod 388 | def gene_recombination(chromosome1: Chromosome, chromosome2: Chromosome) -> (Chromosome, Chromosome): 389 | """ 390 | Two point recombination that occurs along gene boundaries for multigenic chromosomes. 391 | 392 | :param chromosome1: parent 1 393 | :param chromosome2: parent 2 394 | :return: offspring 1, offspring 2 395 | """ 396 | 397 | # choose gene to swap 398 | gene = randint(0, Chromosome.num_genes - 1) 399 | 400 | # initialize children genes 401 | child1_genes = chromosome1.genes 402 | child2_genes = chromosome2.genes 403 | 404 | # perform swap 405 | child1_genes[gene] = chromosome2.genes[gene] 406 | child2_genes[gene] = chromosome1.genes[gene] 407 | 408 | return Chromosome(child1_genes), Chromosome(child2_genes) 409 | 410 | 411 | @staticmethod 412 | def plot_reps(avg_fitnesses: list, best_fitnesses: list, random_search_avg: list = None, random_search_best: list = None) -> None: 413 | """ 414 | Plot all reps with global best solutions. 415 | 416 | :param avg_fitnesses: list of lists containing average fitnesses by generation for each rep 417 | :param best_fitnesses: same as avg_fitnesses but best fitnesses 418 | :param random_search_avg: average fitness value for random search across generations 419 | :param random_search_best: best fitness value for random search by generation 420 | :return: void 421 | """ 422 | 423 | is_random_search = not (random_search_avg is None or random_search_best is None) 424 | 425 | plt.subplots(1, 2, figsize=(16, 8)) 426 | 427 | plt.subplot(1, 2, 1) 428 | plt.title("Average Fitness by Generation") 429 | plt.xlabel("Generation") 430 | plt.ylabel("Average Fitness") 431 | 432 | # plot each rep 433 | for rep in range(GeneExpressionProgram.NUM_RUNS): 434 | plt.plot(range(len(avg_fitnesses[rep])), avg_fitnesses[rep], label="Rep %d Average" % (rep + 1)) 435 | 436 | if is_random_search: 437 | plt.plot(range(len(random_search_avg)), random_search_avg, label="Random Search Average") 438 | #plt.plot(range(len(random_search_best)), random_search_best, label="Random Search Best") 439 | 440 | plt.legend(loc="upper left") 441 | 442 | plt.subplot(1, 2, 2) 443 | plt.title("Best Fitness by Generation") 444 | plt.xlabel("Generation") 445 | plt.ylabel("Best Fitness") 446 | 447 | # plot each rep 448 | for rep in range(GeneExpressionProgram.NUM_RUNS): 449 | plt.plot(range(len(best_fitnesses[rep])), best_fitnesses[rep], label="Rep %d Best" % (rep + 1)) 450 | 451 | if is_random_search: 452 | plt.plot(range(len(random_search_best)), random_search_best, label="Random Search Best") 453 | 454 | plt.legend(loc="upper left") 455 | plt.show() 456 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | Libraries copied to this folder for portability: 2 | anytree 2.2.1: 3 | Author: c0fec0de 4 | Author contact: c0fec0de@gmail.com 5 | License: Apache 2.0 6 | Link: https://pypi.python.org/pypi/anytree/2.2.1 7 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeff-dale/Gene-Expression-Programming/0c4dcb1fcf548470414062cd421283c92f19b12b/lib/__init__.py -------------------------------------------------------------------------------- /lib/anytree/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Powerful and Lightweight Python Tree Data Structure.""" 4 | 5 | __version__ = "2.2.1" 6 | __author__ = "c0fec0de" 7 | __author_email__ = "c0fec0de@gmail.com" 8 | __description__ = """Powerful and Lightweight Python Tree Data Structure..""" 9 | __url__ = "https://github.com/c0fec0de/anytree" 10 | 11 | from .iterators import LevelOrderGroupIter # noqa 12 | from .iterators import LevelOrderIter # noqa 13 | from .iterators import PostOrderIter # noqa 14 | from .iterators import PreOrderIter # noqa 15 | from .iterators import ZigZagGroupIter # noqa 16 | from .node import LoopError # noqa 17 | from .node import Node # noqa 18 | from .node import NodeMixin # noqa 19 | from .render import AbstractStyle # noqa 20 | from .render import AsciiStyle # noqa 21 | from .render import ContRoundStyle # noqa 22 | from .render import ContStyle # noqa 23 | from .render import DoubleStyle # noqa 24 | from .render import RenderTree # noqa 25 | from .resolver import ChildResolverError # noqa 26 | from .resolver import Resolver # noqa 27 | from .resolver import ResolverError # noqa 28 | from .walker import WalkError # noqa 29 | from .walker import Walker # noqa 30 | 31 | # legacy 32 | LevelGroupOrderIter = LevelOrderGroupIter 33 | -------------------------------------------------------------------------------- /lib/anytree/dotexport.py: -------------------------------------------------------------------------------- 1 | from codecs import open 2 | from os import path 3 | from subprocess import check_call 4 | from tempfile import NamedTemporaryFile 5 | 6 | from anytree import PreOrderIter 7 | 8 | 9 | class _Render(object): 10 | 11 | def to_dotfile(self, filename): 12 | """ 13 | Write graph to `filename`. 14 | 15 | >>> from anytree import Node 16 | >>> root = Node("root") 17 | >>> s0 = Node("sub0", parent=root) 18 | >>> s0b = Node("sub0B", parent=s0) 19 | >>> s0a = Node("sub0A", parent=s0) 20 | >>> s1 = Node("sub1", parent=root) 21 | >>> s1a = Node("sub1A", parent=s1) 22 | >>> s1b = Node("sub1B", parent=s1) 23 | >>> s1c = Node("sub1C", parent=s1) 24 | >>> s1ca = Node("sub1Ca", parent=s1c) 25 | 26 | >>> RenderTreeGraph(root).to_dotfile("tree.dot") 27 | 28 | The generated file should be handed over to the `dot` tool from the 29 | http://www.graphviz.org/ package:: 30 | 31 | $ dot tree.dot -T png -o tree.png 32 | """ 33 | with open(filename, "w", "utf-8") as file: 34 | for line in self: 35 | file.write("%s\n" % line) 36 | 37 | def to_picture(self, filename): 38 | """ 39 | Write graph to a temporary file and invoke `dot`. 40 | 41 | The output file type is automatically detected from the file suffix. 42 | 43 | *`graphviz` needs to be installed, before usage of this method.* 44 | """ 45 | fileformat = path.splitext(filename)[1][1:] 46 | with NamedTemporaryFile("wb") as dotfile: 47 | for line in self: 48 | dotfile.write(("%s\n" % line).encode("utf-8")) 49 | dotfile.flush() 50 | cmd = ["dot", dotfile.name, "-T", fileformat, "-o", filename] 51 | check_call(cmd) 52 | 53 | 54 | class RenderTreeGraph(_Render): 55 | 56 | def __init__(self, node, graph="digraph", name="tree", options=None, 57 | indent=4, nodenamefunc=None, nodeattrfunc=None, 58 | edgeattrfunc=None): 59 | """ 60 | Dot Language Exporter. 61 | 62 | Args: 63 | node (Node): start node. 64 | 65 | Keyword Args: 66 | graph: DOT graph type. 67 | 68 | name: DOT graph name. 69 | 70 | options: list of options added to the graph. 71 | 72 | indent (int): number of spaces for indent. 73 | 74 | nodenamefunc: Function to extract node name from `node` object. 75 | The function shall accept one `node` object as 76 | argument and return the name of it. 77 | 78 | nodeattrfunc: Function to decorate a node with attributes. 79 | The function shall accept one `node` object as 80 | argument and return the attributes. 81 | 82 | edgeattrfunc: Function to decorate a edge with attributes. 83 | The function shall accept two `node` objects as 84 | argument. The first the node and the second the child 85 | and return the attributes. 86 | 87 | >>> from anytree import Node 88 | >>> root = Node("root") 89 | >>> s0 = Node("sub0", parent=root, edge=2) 90 | >>> s0b = Node("sub0B", parent=s0, foo=4, edge=109) 91 | >>> s0a = Node("sub0A", parent=s0, edge="") 92 | >>> s1 = Node("sub1", parent=root, edge="") 93 | >>> s1a = Node("sub1A", parent=s1, edge=7) 94 | >>> s1b = Node("sub1B", parent=s1, edge=8) 95 | >>> s1c = Node("sub1C", parent=s1, edge=22) 96 | >>> s1ca = Node("sub1Ca", parent=s1c, edge=42) 97 | 98 | >>> for line in RenderTreeGraph(root): 99 | ... print(line) 100 | digraph tree { 101 | "root"; 102 | "sub0"; 103 | "sub0B"; 104 | "sub0A"; 105 | "sub1"; 106 | "sub1A"; 107 | "sub1B"; 108 | "sub1C"; 109 | "sub1Ca"; 110 | "root" -> "sub0"; 111 | "root" -> "sub1"; 112 | "sub0" -> "sub0B"; 113 | "sub0" -> "sub0A"; 114 | "sub1" -> "sub1A"; 115 | "sub1" -> "sub1B"; 116 | "sub1" -> "sub1C"; 117 | "sub1C" -> "sub1Ca"; 118 | } 119 | 120 | >>> def nodenamefunc(node): 121 | ... return '%s:%s' % (node.name, node.depth) 122 | >>> def edgeattrfunc(node, child): 123 | ... return 'label="%s:%s"' % (node.name, child.name) 124 | >>> for line in RenderTreeGraph(root, options=["rankdir=LR;"], 125 | ... nodenamefunc=nodenamefunc, 126 | ... nodeattrfunc=lambda node: "shape=box", 127 | ... edgeattrfunc=edgeattrfunc): 128 | ... print(line) 129 | digraph tree { 130 | rankdir=LR; 131 | "root:0" [shape=box]; 132 | "sub0:1" [shape=box]; 133 | "sub0B:2" [shape=box]; 134 | "sub0A:2" [shape=box]; 135 | "sub1:1" [shape=box]; 136 | "sub1A:2" [shape=box]; 137 | "sub1B:2" [shape=box]; 138 | "sub1C:2" [shape=box]; 139 | "sub1Ca:3" [shape=box]; 140 | "root:0" -> "sub0:1" [label="root:sub0"]; 141 | "root:0" -> "sub1:1" [label="root:sub1"]; 142 | "sub0:1" -> "sub0B:2" [label="sub0:sub0B"]; 143 | "sub0:1" -> "sub0A:2" [label="sub0:sub0A"]; 144 | "sub1:1" -> "sub1A:2" [label="sub1:sub1A"]; 145 | "sub1:1" -> "sub1B:2" [label="sub1:sub1B"]; 146 | "sub1:1" -> "sub1C:2" [label="sub1:sub1C"]; 147 | "sub1C:2" -> "sub1Ca:3" [label="sub1C:sub1Ca"]; 148 | } 149 | """ 150 | self.node = node 151 | self.graph = graph 152 | self.name = name 153 | self.options = options 154 | self.indent = indent 155 | self.nodenamefunc = nodenamefunc 156 | self.nodeattrfunc = nodeattrfunc 157 | self.edgeattrfunc = edgeattrfunc 158 | 159 | def __iter__(self): 160 | # prepare 161 | indent = " " * self.indent 162 | nodenamefunc = self.nodenamefunc 163 | if not nodenamefunc: 164 | def nodenamefunc(node): 165 | return node.name 166 | nodeattrfunc = self.nodeattrfunc 167 | if not nodeattrfunc: 168 | def nodeattrfunc(node): 169 | return None 170 | edgeattrfunc = self.edgeattrfunc 171 | if not edgeattrfunc: 172 | def edgeattrfunc(node, child): 173 | return None 174 | return self.__iter(indent, nodenamefunc, nodeattrfunc, edgeattrfunc) 175 | 176 | def __iter(self, indent, nodenamefunc, nodeattrfunc, edgeattrfunc): 177 | yield "{self.graph} {self.name} {{".format(self=self) 178 | for option in self.__iter_options(indent): 179 | yield option 180 | for node in self.__iter_nodes(indent, nodenamefunc, nodeattrfunc): 181 | yield node 182 | for edge in self.__iter_edges(indent, nodenamefunc, edgeattrfunc): 183 | yield edge 184 | yield "}" 185 | 186 | def __iter_options(self, indent): 187 | options = self.options 188 | if options: 189 | for option in options: 190 | yield "%s%s" % (indent, option) 191 | 192 | def __iter_nodes(self, indent, nodenamefunc, nodeattrfunc): 193 | for node in PreOrderIter(self.node): 194 | nodename = nodenamefunc(node) 195 | nodeattr = nodeattrfunc(node) 196 | nodeattr = " [%s]" % nodeattr if nodeattr is not None else "" 197 | yield '%s"%s"%s;' % (indent, nodename, nodeattr) 198 | 199 | def __iter_edges(self, indent, nodenamefunc, edgeattrfunc): 200 | for node in PreOrderIter(self.node): 201 | nodename = nodenamefunc(node) 202 | for child in node.children: 203 | childname = nodenamefunc(child) 204 | edgeattr = edgeattrfunc(node, child) 205 | edgeattr = " [%s]" % edgeattr if edgeattr is not None else "" 206 | yield '%s"%s" -> "%s"%s;' % (indent, nodename, childname, 207 | edgeattr) 208 | -------------------------------------------------------------------------------- /lib/anytree/iterators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Tree Iteration. 4 | 5 | * :any:`PreOrderIter`: iterate over tree using pre-order strategy (self, children) 6 | * :any:`PostOrderIter`: iterate over tree using post-order strategy (children, self) 7 | * :any:`LevelOrderIter`: iterate over tree using level-order strategy 8 | * :any:`LevelOrderGroupIter`: iterate over tree using level-order strategy returning group for every level 9 | * :any:`ZigZagGroupIter`: iterate over tree using level-order strategy returning group for every level 10 | """ 11 | import six 12 | 13 | 14 | class AbstractIter(six.Iterator): 15 | 16 | def __init__(self, node, filter_=None, stop=None, maxlevel=None): 17 | """ 18 | Base class for all iterators. 19 | 20 | Iterate over tree starting at `node`. 21 | 22 | Keyword Args: 23 | filter_: function called with every `node` as argument, `node` is returned if `True`. 24 | stop: stop iteration at `node` if `stop` function returns `True` for `node`. 25 | maxlevel (int): maximum decending in the node hierarchy. 26 | """ 27 | self.node = node 28 | self.filter_ = filter_ 29 | self.stop = stop 30 | self.maxlevel = maxlevel 31 | self.__iter = None 32 | 33 | def __init(self): 34 | node = self.node 35 | filter_ = self.filter_ 36 | stop = self.stop 37 | maxlevel = self.maxlevel 38 | if filter_ is None: 39 | def filter_(node): 40 | return True 41 | if stop is None: 42 | def stop(node): 43 | return False 44 | children = [] if AbstractIter._abort_at_level(1, maxlevel) else AbstractIter._get_children([node], stop) 45 | return self._iter(children, filter_, stop, maxlevel) 46 | 47 | def __iter__(self): 48 | return self.__init() 49 | 50 | def __next__(self): 51 | if self.__iter is None: 52 | self.__iter = self.__init() 53 | return next(self.__iter) 54 | 55 | @staticmethod 56 | def _iter(children, filter_, stop, maxlevel): 57 | raise NotImplementedError() # pragma: no cover 58 | 59 | @staticmethod 60 | def _abort_at_level(level, maxlevel): 61 | return maxlevel is not None and level > maxlevel 62 | 63 | @staticmethod 64 | def _get_children(children, stop): 65 | return [child for child in children if not stop(child)] 66 | 67 | 68 | class PreOrderIter(AbstractIter): 69 | 70 | """ 71 | Iterate over tree applying pre-order strategy starting at `node`. 72 | 73 | Start at root and go-down until reaching a leaf node. 74 | Step upwards then, and search for the next leafs. 75 | 76 | >>> from anytree import Node, RenderTree, AsciiStyle 77 | >>> f = Node("f") 78 | >>> b = Node("b", parent=f) 79 | >>> a = Node("a", parent=b) 80 | >>> d = Node("d", parent=b) 81 | >>> c = Node("c", parent=d) 82 | >>> e = Node("e", parent=d) 83 | >>> g = Node("g", parent=f) 84 | >>> i = Node("i", parent=g) 85 | >>> h = Node("h", parent=i) 86 | >>> print(RenderTree(f, style=AsciiStyle()).by_attr()) 87 | f 88 | |-- b 89 | | |-- a 90 | | +-- d 91 | | |-- c 92 | | +-- e 93 | +-- g 94 | +-- i 95 | +-- h 96 | >>> [node.name for node in PreOrderIter(f)] 97 | ['f', 'b', 'a', 'd', 'c', 'e', 'g', 'i', 'h'] 98 | >>> [node.name for node in PreOrderIter(f, maxlevel=3)] 99 | ['f', 'b', 'a', 'd', 'g', 'i'] 100 | >>> [node.name for node in PreOrderIter(f, filter_=lambda n: n.name not in ('e', 'g'))] 101 | ['f', 'b', 'a', 'd', 'c', 'i', 'h'] 102 | >>> [node.name for node in PreOrderIter(f, stop=lambda n: n.name == 'd')] 103 | ['f', 'b', 'a', 'g', 'i', 'h'] 104 | """ 105 | 106 | @staticmethod 107 | def _iter(children, filter_, stop, maxlevel): 108 | stack = [children] 109 | while stack: 110 | children = stack[-1] 111 | if children: 112 | child = children.pop(0) 113 | if filter_(child): 114 | yield child 115 | if not AbstractIter._abort_at_level(len(stack) + 1, maxlevel): 116 | grandchildren = AbstractIter._get_children(child.children, stop) 117 | if grandchildren: 118 | stack.append(grandchildren) 119 | else: 120 | stack.pop() 121 | 122 | 123 | class PostOrderIter(AbstractIter): 124 | 125 | """ 126 | Iterate over tree applying post-order strategy starting at `node`. 127 | 128 | >>> from anytree import Node, RenderTree, AsciiStyle 129 | >>> f = Node("f") 130 | >>> b = Node("b", parent=f) 131 | >>> a = Node("a", parent=b) 132 | >>> d = Node("d", parent=b) 133 | >>> c = Node("c", parent=d) 134 | >>> e = Node("e", parent=d) 135 | >>> g = Node("g", parent=f) 136 | >>> i = Node("i", parent=g) 137 | >>> h = Node("h", parent=i) 138 | >>> print(RenderTree(f, style=AsciiStyle()).by_attr()) 139 | f 140 | |-- b 141 | | |-- a 142 | | +-- d 143 | | |-- c 144 | | +-- e 145 | +-- g 146 | +-- i 147 | +-- h 148 | >>> [node.name for node in PostOrderIter(f)] 149 | ['a', 'c', 'e', 'd', 'b', 'h', 'i', 'g', 'f'] 150 | >>> [node.name for node in PostOrderIter(f, maxlevel=3)] 151 | ['a', 'd', 'b', 'i', 'g', 'f'] 152 | >>> [node.name for node in PostOrderIter(f, filter_=lambda n: n.name not in ('e', 'g'))] 153 | ['a', 'c', 'd', 'b', 'h', 'i', 'f'] 154 | >>> [node.name for node in PostOrderIter(f, stop=lambda n: n.name == 'd')] 155 | ['a', 'b', 'h', 'i', 'g', 'f'] 156 | """ 157 | 158 | @staticmethod 159 | def _iter(children, filter_, stop, maxlevel): 160 | return PostOrderIter.__next(children, 1, filter_, stop, maxlevel) 161 | 162 | @staticmethod 163 | def __next(children, level, filter_, stop, maxlevel): 164 | if not AbstractIter._abort_at_level(level, maxlevel): 165 | for child in children: 166 | grandchildren = AbstractIter._get_children(child.children, stop) 167 | for grandchild in PostOrderIter.__next(grandchildren, level + 1, filter_, stop, maxlevel): 168 | yield grandchild 169 | if filter_(child): 170 | yield child 171 | 172 | 173 | class LevelOrderIter(AbstractIter): 174 | 175 | """ 176 | Iterate over tree applying level-order strategy starting at `node`. 177 | 178 | >>> from anytree import Node, RenderTree, AsciiStyle 179 | >>> f = Node("f") 180 | >>> b = Node("b", parent=f) 181 | >>> a = Node("a", parent=b) 182 | >>> d = Node("d", parent=b) 183 | >>> c = Node("c", parent=d) 184 | >>> e = Node("e", parent=d) 185 | >>> g = Node("g", parent=f) 186 | >>> i = Node("i", parent=g) 187 | >>> h = Node("h", parent=i) 188 | >>> print(RenderTree(f, style=AsciiStyle()).by_attr()) 189 | f 190 | |-- b 191 | | |-- a 192 | | +-- d 193 | | |-- c 194 | | +-- e 195 | +-- g 196 | +-- i 197 | +-- h 198 | >>> [node.name for node in LevelOrderIter(f)] 199 | ['f', 'b', 'g', 'a', 'd', 'i', 'c', 'e', 'h'] 200 | >>> [node.name for node in LevelOrderIter(f, maxlevel=3)] 201 | ['f', 'b', 'g', 'a', 'd', 'i'] 202 | >>> [node.name for node in LevelOrderIter(f, filter_=lambda n: n.name not in ('e', 'g'))] 203 | ['f', 'b', 'a', 'd', 'i', 'c', 'h'] 204 | >>> [node.name for node in LevelOrderIter(f, stop=lambda n: n.name == 'd')] 205 | ['f', 'b', 'g', 'a', 'i', 'h'] 206 | """ 207 | 208 | @staticmethod 209 | def _iter(children, filter_, stop, maxlevel): 210 | level = 1 211 | while children: 212 | next_children = [] 213 | for child in children: 214 | if filter_(child): 215 | yield child 216 | next_children += AbstractIter._get_children(child.children, stop) 217 | children = next_children 218 | level += 1 219 | if AbstractIter._abort_at_level(level, maxlevel): 220 | break 221 | 222 | 223 | class LevelOrderGroupIter(AbstractIter): 224 | 225 | """ 226 | Iterate over tree applying level-order strategy with grouping starting at `node`. 227 | 228 | Return a tuple of nodes for each level. The first tuple contains the 229 | nodes at level 0 (always `node`). The second tuple contains the nodes at level 1 230 | (children of `node`). The next level contains the children of the children, and so on. 231 | 232 | >>> from anytree import Node, RenderTree, AsciiStyle 233 | >>> f = Node("f") 234 | >>> b = Node("b", parent=f) 235 | >>> a = Node("a", parent=b) 236 | >>> d = Node("d", parent=b) 237 | >>> c = Node("c", parent=d) 238 | >>> e = Node("e", parent=d) 239 | >>> g = Node("g", parent=f) 240 | >>> i = Node("i", parent=g) 241 | >>> h = Node("h", parent=i) 242 | >>> print(RenderTree(f, style=AsciiStyle()).by_attr()) 243 | f 244 | |-- b 245 | | |-- a 246 | | +-- d 247 | | |-- c 248 | | +-- e 249 | +-- g 250 | +-- i 251 | +-- h 252 | >>> [[node.name for node in children] for children in LevelOrderGroupIter(f)] 253 | [['f'], ['b', 'g'], ['a', 'd', 'i'], ['c', 'e', 'h']] 254 | >>> [[node.name for node in children] for children in LevelOrderGroupIter(f, maxlevel=3)] 255 | [['f'], ['b', 'g'], ['a', 'd', 'i']] 256 | >>> [[node.name for node in children] 257 | ... for children in LevelOrderGroupIter(f, filter_=lambda n: n.name not in ('e', 'g'))] 258 | [['f'], ['b'], ['a', 'd', 'i'], ['c', 'h']] 259 | >>> [[node.name for node in children] 260 | ... for children in LevelOrderGroupIter(f, stop=lambda n: n.name == 'd')] 261 | [['f'], ['b', 'g'], ['a', 'i'], ['h']] 262 | """ 263 | 264 | @staticmethod 265 | def _iter(children, filter_, stop, maxlevel): 266 | level = 1 267 | while children: 268 | yield tuple([child for child in children if filter_(child)]) 269 | level += 1 270 | if AbstractIter._abort_at_level(level, maxlevel): 271 | break 272 | children = LevelOrderGroupIter._get_grandchildren(children, stop) 273 | 274 | @staticmethod 275 | def _get_grandchildren(children, stop): 276 | next_children = [] 277 | for child in children: 278 | next_children = next_children + AbstractIter._get_children(child.children, stop) 279 | return next_children 280 | 281 | 282 | class ZigZagGroupIter(AbstractIter): 283 | 284 | """ 285 | Iterate over tree applying Zig-Zag strategy with grouping starting at `node`. 286 | 287 | Return a tuple of nodes for each level. The first tuple contains the 288 | nodes at level 0 (always `node`). The second tuple contains the nodes at level 1 289 | (children of `node`) in reversed order. 290 | The next level contains the children of the children in forward order, and so on. 291 | 292 | >>> from anytree import Node, RenderTree, AsciiStyle 293 | >>> f = Node("f") 294 | >>> b = Node("b", parent=f) 295 | >>> a = Node("a", parent=b) 296 | >>> d = Node("d", parent=b) 297 | >>> c = Node("c", parent=d) 298 | >>> e = Node("e", parent=d) 299 | >>> g = Node("g", parent=f) 300 | >>> i = Node("i", parent=g) 301 | >>> h = Node("h", parent=i) 302 | >>> print(RenderTree(f, style=AsciiStyle()).by_attr()) 303 | f 304 | |-- b 305 | | |-- a 306 | | +-- d 307 | | |-- c 308 | | +-- e 309 | +-- g 310 | +-- i 311 | +-- h 312 | >>> [[node.name for node in children] for children in ZigZagGroupIter(f)] 313 | [['f'], ['g', 'b'], ['a', 'd', 'i'], ['h', 'e', 'c']] 314 | >>> [[node.name for node in children] for children in ZigZagGroupIter(f, maxlevel=3)] 315 | [['f'], ['g', 'b'], ['a', 'd', 'i']] 316 | >>> [[node.name for node in children] 317 | ... for children in ZigZagGroupIter(f, filter_=lambda n: n.name not in ('e', 'g'))] 318 | [['f'], ['b'], ['a', 'd', 'i'], ['h', 'c']] 319 | >>> [[node.name for node in children] 320 | ... for children in ZigZagGroupIter(f, stop=lambda n: n.name == 'd')] 321 | [['f'], ['g', 'b'], ['a', 'i'], ['h']] 322 | """ 323 | 324 | @staticmethod 325 | def _iter(children, filter_, stop, maxlevel): 326 | _iter = LevelOrderGroupIter._iter(children, filter_, stop, maxlevel) 327 | while True: 328 | yield next(_iter) 329 | yield tuple(reversed(next(_iter))) 330 | -------------------------------------------------------------------------------- /lib/anytree/node.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Node Classes. 4 | 5 | * :any:`Node`: a simple tree node 6 | * :any:`NodeMixin`: extends any python class to a tree node. 7 | """ 8 | 9 | from __future__ import print_function 10 | from .iterators import PreOrderIter 11 | import warnings 12 | 13 | 14 | class NodeMixin(object): 15 | 16 | separator = "/" 17 | 18 | u""" 19 | The :any:`NodeMixin` class extends any Python class to a tree node. 20 | 21 | The only tree relevant information is the `parent` attribute. 22 | If `None` the :any:`NodeMixin` is root node. 23 | If set to another node, the :any:`NodeMixin` becomes the child of it. 24 | 25 | >>> from anytree import Node, RenderTree 26 | >>> class MyBaseClass(object): 27 | ... foo = 4 28 | >>> class MyClass(MyBaseClass, NodeMixin): # Add Node feature 29 | ... def __init__(self, name, length, width, parent=None): 30 | ... super(MyClass, self).__init__() 31 | ... self.name = name 32 | ... self.length = length 33 | ... self.width = width 34 | ... self.parent = parent 35 | 36 | >>> my0 = MyClass('my0', 0, 0) 37 | >>> my1 = MyClass('my1', 1, 0, parent=my0) 38 | >>> my2 = MyClass('my2', 0, 2, parent=my0) 39 | 40 | >>> for pre, _, node in RenderTree(my0): 41 | ... treestr = u"%s%s" % (pre, node.name) 42 | ... print(treestr.ljust(8), node.length, node.width) 43 | my0 0 0 44 | ├── my1 1 0 45 | └── my2 0 2 46 | """ 47 | 48 | @property 49 | def parent(self): 50 | u""" 51 | Parent Node. 52 | 53 | On set, the node is detached from any previous parent node and attached 54 | to the new node. 55 | 56 | >>> from anytree import Node, RenderTree 57 | >>> udo = Node("Udo") 58 | >>> marc = Node("Marc") 59 | >>> lian = Node("Lian", parent=marc) 60 | >>> print(RenderTree(udo)) 61 | Node('/Udo') 62 | >>> print(RenderTree(marc)) 63 | Node('/Marc') 64 | └── Node('/Marc/Lian') 65 | 66 | **Attach** 67 | 68 | >>> marc.parent = udo 69 | >>> print(RenderTree(udo)) 70 | Node('/Udo') 71 | └── Node('/Udo/Marc') 72 | └── Node('/Udo/Marc/Lian') 73 | 74 | **Detach** 75 | 76 | To make a node to a root node, just set this attribute to `None`. 77 | 78 | >>> marc.is_root 79 | False 80 | >>> marc.parent = None 81 | >>> marc.is_root 82 | True 83 | """ 84 | try: 85 | return self._parent 86 | except AttributeError: 87 | return None 88 | 89 | @parent.setter 90 | def parent(self, value): 91 | try: 92 | parent = self._parent 93 | except AttributeError: 94 | parent = None 95 | if value is None: 96 | # make this node to root node 97 | self.__detach(parent) 98 | elif parent is not value: 99 | # change parent node 100 | self.__check_loop(value) 101 | self.__detach(parent) 102 | self.__attach(value) 103 | else: 104 | # keep parent 105 | pass 106 | # apply 107 | self._parent = value 108 | 109 | def __check_loop(self, node): 110 | if node is self: 111 | msg = "Cannot set parent. %r cannot be parent of itself." 112 | raise LoopError(msg % self) 113 | if self in node.path: 114 | msg = "Cannot set parent. %r is parent of %r." 115 | raise LoopError(msg % (self, node)) 116 | 117 | def __detach(self, parent): 118 | if parent: 119 | self._pre_detach(parent) 120 | parentchildren = parent._children 121 | assert self in parentchildren, "Tree internal data is corrupt." 122 | parentchildren.remove(self) 123 | self._post_detach(parent) 124 | 125 | def __attach(self, parent): 126 | self._pre_attach(parent) 127 | parentchildren = parent._children 128 | assert self not in parentchildren, "Tree internal data is corrupt." 129 | parentchildren.append(self) 130 | self._post_attach(parent) 131 | 132 | @property 133 | def _children(self): 134 | try: 135 | return self.__children 136 | except AttributeError: 137 | self.__children = [] 138 | return self.__children 139 | 140 | @property 141 | def children(self): 142 | """ 143 | All child nodes. 144 | 145 | >>> dan = Node("Dan") 146 | >>> jet = Node("Jet", parent=dan) 147 | >>> jan = Node("Jan", parent=dan) 148 | >>> joe = Node("Joe", parent=dan) 149 | >>> dan.children 150 | (Node('/Dan/Jet'), Node('/Dan/Jan'), Node('/Dan/Joe')) 151 | """ 152 | return tuple(self._children) 153 | 154 | @children.setter 155 | def children(self, children): 156 | self._pre_attach_children(children) 157 | 158 | old_children = self.children 159 | del self.children 160 | 161 | try: 162 | for child in children: 163 | assert isinstance(child, NodeMixin), ("Cannot add non-node object %r." % child) 164 | child.parent = self 165 | assert len(self.children) == len(children) 166 | self._post_attach_children(children) 167 | except: 168 | self.children = old_children 169 | raise 170 | 171 | @children.deleter 172 | def children(self): 173 | children = self.children 174 | self._pre_detach_children(children) 175 | for child in self.children: 176 | child.parent = None 177 | assert len(self.children) == 0 178 | self._post_detach_children(children) 179 | 180 | def _pre_detach_children(self, children): 181 | """Method call before detaching `children`.""" 182 | pass 183 | 184 | def _post_detach_children(self, children): 185 | """Method call after detaching `children`.""" 186 | pass 187 | 188 | def _pre_attach_children(self, children): 189 | """Method call before attaching `children`.""" 190 | pass 191 | 192 | def _post_attach_children(self, children): 193 | """Method call after attaching `children`.""" 194 | pass 195 | 196 | @property 197 | def path(self): 198 | """ 199 | Path of this `Node`. 200 | 201 | >>> udo = Node("Udo") 202 | >>> marc = Node("Marc", parent=udo) 203 | >>> lian = Node("Lian", parent=marc) 204 | >>> udo.path 205 | (Node('/Udo'),) 206 | >>> marc.path 207 | (Node('/Udo'), Node('/Udo/Marc')) 208 | >>> lian.path 209 | (Node('/Udo'), Node('/Udo/Marc'), Node('/Udo/Marc/Lian')) 210 | """ 211 | return self._path 212 | 213 | @property 214 | def _path(self): 215 | path = [] 216 | node = self 217 | while node: 218 | path.insert(0, node) 219 | node = node.parent 220 | return tuple(path) 221 | 222 | @property 223 | def ancestors(self): 224 | """ 225 | All parent nodes and their parent nodes. 226 | 227 | >>> udo = Node("Udo") 228 | >>> marc = Node("Marc", parent=udo) 229 | >>> lian = Node("Lian", parent=marc) 230 | >>> udo.ancestors 231 | () 232 | >>> marc.ancestors 233 | (Node('/Udo'),) 234 | >>> lian.ancestors 235 | (Node('/Udo'), Node('/Udo/Marc')) 236 | """ 237 | return self._path[:-1] 238 | 239 | @property 240 | def anchestors(self): 241 | """ 242 | All parent nodes and their parent nodes - see :any:`anchestors`. 243 | 244 | The attribute `anchestors` is just a typo of `ancestors`. Please use `ancestors`. 245 | This attribute will be removed in the 2.0.0 release. 246 | """ 247 | warnings.warn(".anchestors was a typo and will be removed in version 2.0.0", DeprecationWarning) 248 | return self.ancestors 249 | 250 | @property 251 | def descendants(self): 252 | """ 253 | All child nodes and all their child nodes. 254 | 255 | >>> udo = Node("Udo") 256 | >>> marc = Node("Marc", parent=udo) 257 | >>> lian = Node("Lian", parent=marc) 258 | >>> loui = Node("Loui", parent=marc) 259 | >>> soe = Node("Soe", parent=lian) 260 | >>> udo.descendants 261 | (Node('/Udo/Marc'), Node('/Udo/Marc/Lian'), Node('/Udo/Marc/Lian/Soe'), Node('/Udo/Marc/Loui')) 262 | >>> marc.descendants 263 | (Node('/Udo/Marc/Lian'), Node('/Udo/Marc/Lian/Soe'), Node('/Udo/Marc/Loui')) 264 | >>> lian.descendants 265 | (Node('/Udo/Marc/Lian/Soe'),) 266 | """ 267 | return tuple(PreOrderIter(self))[1:] 268 | 269 | @property 270 | def root(self): 271 | """ 272 | Tree Root Node. 273 | 274 | >>> udo = Node("Udo") 275 | >>> marc = Node("Marc", parent=udo) 276 | >>> lian = Node("Lian", parent=marc) 277 | >>> udo.root 278 | Node('/Udo') 279 | >>> marc.root 280 | Node('/Udo') 281 | >>> lian.root 282 | Node('/Udo') 283 | """ 284 | if self.parent: 285 | return self._path[0] 286 | else: 287 | return self 288 | 289 | @property 290 | def siblings(self): 291 | """ 292 | Tuple of nodes with the same parent. 293 | 294 | >>> udo = Node("Udo") 295 | >>> marc = Node("Marc", parent=udo) 296 | >>> lian = Node("Lian", parent=marc) 297 | >>> loui = Node("Loui", parent=marc) 298 | >>> lazy = Node("Lazy", parent=marc) 299 | >>> udo.siblings 300 | () 301 | >>> marc.siblings 302 | () 303 | >>> lian.siblings 304 | (Node('/Udo/Marc/Loui'), Node('/Udo/Marc/Lazy')) 305 | >>> loui.siblings 306 | (Node('/Udo/Marc/Lian'), Node('/Udo/Marc/Lazy')) 307 | """ 308 | parent = self.parent 309 | if parent is None: 310 | return tuple() 311 | else: 312 | return tuple([node for node in parent._children if node != self]) 313 | 314 | @property 315 | def is_leaf(self): 316 | """ 317 | `Node` has no childrean (External Node). 318 | 319 | >>> udo = Node("Udo") 320 | >>> marc = Node("Marc", parent=udo) 321 | >>> lian = Node("Lian", parent=marc) 322 | >>> udo.is_leaf 323 | False 324 | >>> marc.is_leaf 325 | False 326 | >>> lian.is_leaf 327 | True 328 | """ 329 | return len(self._children) == 0 330 | 331 | @property 332 | def is_root(self): 333 | """ 334 | `Node` is tree root. 335 | 336 | >>> udo = Node("Udo") 337 | >>> marc = Node("Marc", parent=udo) 338 | >>> lian = Node("Lian", parent=marc) 339 | >>> udo.is_root 340 | True 341 | >>> marc.is_root 342 | False 343 | >>> lian.is_root 344 | False 345 | """ 346 | return self.parent is None 347 | 348 | @property 349 | def height(self): 350 | """ 351 | Number of edges on the longest path to a leaf `Node`. 352 | 353 | >>> udo = Node("Udo") 354 | >>> marc = Node("Marc", parent=udo) 355 | >>> lian = Node("Lian", parent=marc) 356 | >>> udo.height 357 | 2 358 | >>> marc.height 359 | 1 360 | >>> lian.height 361 | 0 362 | """ 363 | if self._children: 364 | return max([child.height for child in self._children]) + 1 365 | else: 366 | return 0 367 | 368 | @property 369 | def depth(self): 370 | """ 371 | Number of edges to the root `Node`. 372 | 373 | >>> udo = Node("Udo") 374 | >>> marc = Node("Marc", parent=udo) 375 | >>> lian = Node("Lian", parent=marc) 376 | >>> udo.depth 377 | 0 378 | >>> marc.depth 379 | 1 380 | >>> lian.depth 381 | 2 382 | """ 383 | return len(self._path) - 1 384 | 385 | def _pre_detach(self, parent): 386 | """Method call before detaching from `parent`.""" 387 | pass 388 | 389 | def _post_detach(self, parent): 390 | """Method call after detaching from `parent`.""" 391 | pass 392 | 393 | def _pre_attach(self, parent): 394 | """Method call before attaching to `parent`.""" 395 | pass 396 | 397 | def _post_attach(self, parent): 398 | """Method call after attaching to `parent`.""" 399 | pass 400 | 401 | 402 | class Node(NodeMixin, object): 403 | 404 | def __init__(self, name, parent=None, **kwargs): 405 | u""" 406 | A simple tree node with a `name` and any `kwargs`. 407 | 408 | >>> from anytree import Node, RenderTree 409 | >>> root = Node("root") 410 | >>> s0 = Node("sub0", parent=root) 411 | >>> s0b = Node("sub0B", parent=s0, foo=4, bar=109) 412 | >>> s0a = Node("sub0A", parent=s0) 413 | >>> s1 = Node("sub1", parent=root) 414 | >>> s1a = Node("sub1A", parent=s1) 415 | >>> s1b = Node("sub1B", parent=s1, bar=8) 416 | >>> s1c = Node("sub1C", parent=s1) 417 | >>> s1ca = Node("sub1Ca", parent=s1c) 418 | 419 | >>> print(RenderTree(root)) 420 | Node('/root') 421 | ├── Node('/root/sub0') 422 | │ ├── Node('/root/sub0/sub0B', bar=109, foo=4) 423 | │ └── Node('/root/sub0/sub0A') 424 | └── Node('/root/sub1') 425 | ├── Node('/root/sub1/sub1A') 426 | ├── Node('/root/sub1/sub1B', bar=8) 427 | └── Node('/root/sub1/sub1C') 428 | └── Node('/root/sub1/sub1C/sub1Ca') 429 | """ 430 | self.name = name 431 | self.parent = parent 432 | self.__dict__.update(kwargs) 433 | 434 | @property 435 | def name(self): 436 | """Name.""" 437 | return self._name 438 | 439 | @name.setter 440 | def name(self, value): 441 | self._name = value 442 | 443 | def __repr__(self): 444 | classname = self.__class__.__name__ 445 | args = ["%r" % self.separator.join([""] + [str(node.name) for node in self.path])] 446 | for key, value in filter(lambda item: not item[0].startswith("_"), 447 | sorted(self.__dict__.items(), 448 | key=lambda item: item[0])): 449 | args.append("%s=%r" % (key, value)) 450 | return "%s(%s)" % (classname, ", ".join(args)) 451 | 452 | 453 | class LoopError(RuntimeError): 454 | 455 | """Tree contains infinite loop.""" 456 | 457 | pass 458 | -------------------------------------------------------------------------------- /lib/anytree/render.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Tree Rendering. 4 | 5 | * :any:`RenderTree` using the following styles: 6 | * :any:`AsciiStyle` 7 | * :any:`ContStyle` 8 | * :any:`ContRoundStyle` 9 | * :any:`DoubleStyle` 10 | """ 11 | 12 | import six 13 | 14 | 15 | class AbstractStyle(object): 16 | 17 | def __init__(self, vertical, cont, end): 18 | """ 19 | Tree Render Style. 20 | 21 | Args: 22 | 23 | vertical: Sign for vertical line. 24 | 25 | cont: Chars for a continued branch. 26 | 27 | end: Chars for the last branch. 28 | """ 29 | super(AbstractStyle, self).__init__() 30 | self.vertical = vertical 31 | self.cont = cont 32 | self.end = end 33 | assert (len(cont) == len(vertical) and len(cont) == len(end)), ( 34 | "'%s', '%s' and '%s' need to have equal length" % (vertical, cont, 35 | end)) 36 | 37 | @property 38 | def empty(self): 39 | """Empty string as placeholder.""" 40 | return ' ' * len(self.end) 41 | 42 | def __repr__(self): 43 | classname = self.__class__.__name__ 44 | return "%s()" % classname 45 | 46 | 47 | class AsciiStyle(AbstractStyle): 48 | 49 | def __init__(self): 50 | """ 51 | Ascii style. 52 | 53 | >>> from anytree import Node, RenderTree 54 | >>> root = Node("root") 55 | >>> s0 = Node("sub0", parent=root) 56 | >>> s0b = Node("sub0B", parent=s0) 57 | >>> s0a = Node("sub0A", parent=s0) 58 | >>> s1 = Node("sub1", parent=root) 59 | 60 | >>> print(RenderTree(root, style=AsciiStyle())) 61 | Node('/root') 62 | |-- Node('/root/sub0') 63 | | |-- Node('/root/sub0/sub0B') 64 | | +-- Node('/root/sub0/sub0A') 65 | +-- Node('/root/sub1') 66 | """ 67 | super(AsciiStyle, self).__init__(u'| ', u'|-- ', u'+-- ') 68 | 69 | 70 | class ContStyle(AbstractStyle): 71 | 72 | def __init__(self): 73 | u""" 74 | Continued style, without gaps. 75 | 76 | >>> from anytree import Node, RenderTree 77 | >>> root = Node("root") 78 | >>> s0 = Node("sub0", parent=root) 79 | >>> s0b = Node("sub0B", parent=s0) 80 | >>> s0a = Node("sub0A", parent=s0) 81 | >>> s1 = Node("sub1", parent=root) 82 | 83 | >>> print(RenderTree(root, style=ContStyle())) 84 | Node('/root') 85 | ├── Node('/root/sub0') 86 | │ ├── Node('/root/sub0/sub0B') 87 | │ └── Node('/root/sub0/sub0A') 88 | └── Node('/root/sub1') 89 | """ 90 | super(ContStyle, self).__init__(u'\u2502 ', 91 | u'\u251c\u2500\u2500 ', 92 | u'\u2514\u2500\u2500 ') 93 | 94 | 95 | class ContRoundStyle(AbstractStyle): 96 | 97 | def __init__(self): 98 | u""" 99 | Continued style, without gaps, round edges. 100 | 101 | >>> from anytree import Node, RenderTree 102 | >>> root = Node("root") 103 | >>> s0 = Node("sub0", parent=root) 104 | >>> s0b = Node("sub0B", parent=s0) 105 | >>> s0a = Node("sub0A", parent=s0) 106 | >>> s1 = Node("sub1", parent=root) 107 | 108 | >>> print(RenderTree(root, style=ContRoundStyle())) 109 | Node('/root') 110 | ├── Node('/root/sub0') 111 | │ ├── Node('/root/sub0/sub0B') 112 | │ ╰── Node('/root/sub0/sub0A') 113 | ╰── Node('/root/sub1') 114 | """ 115 | super(ContRoundStyle, self).__init__(u'\u2502 ', 116 | u'\u251c\u2500\u2500 ', 117 | u'\u2570\u2500\u2500 ') 118 | 119 | 120 | class DoubleStyle(AbstractStyle): 121 | 122 | def __init__(self): 123 | u""" 124 | Double line style, without gaps. 125 | 126 | >>> from anytree import Node, RenderTree 127 | >>> root = Node("root") 128 | >>> s0 = Node("sub0", parent=root) 129 | >>> s0b = Node("sub0B", parent=s0) 130 | >>> s0a = Node("sub0A", parent=s0) 131 | >>> s1 = Node("sub1", parent=root) 132 | 133 | >>> print(RenderTree(root, style=DoubleStyle)) 134 | Node('/root') 135 | ╠══ Node('/root/sub0') 136 | ║ ╠══ Node('/root/sub0/sub0B') 137 | ║ ╚══ Node('/root/sub0/sub0A') 138 | ╚══ Node('/root/sub1') 139 | 140 | """ 141 | super(DoubleStyle, self).__init__(u'\u2551 ', 142 | u'\u2560\u2550\u2550 ', 143 | u'\u255a\u2550\u2550 ') 144 | 145 | 146 | @six.python_2_unicode_compatible 147 | class RenderTree(object): 148 | 149 | def __init__(self, node, style=ContStyle(), childiter=list): 150 | u""" 151 | Render tree starting at `node`. 152 | 153 | Keyword Args: 154 | style (AbstractStyle): Render Style. 155 | 156 | childiter: Child iterator. 157 | 158 | :any:`RenderTree` is an iterator, returning a tuple with 3 items: 159 | 160 | `pre` 161 | tree prefix. 162 | 163 | `fill` 164 | filling for multiline entries. 165 | 166 | `node` 167 | :any:`NodeMixin` object. 168 | 169 | It is up to the user to assemble these parts to a whole. 170 | 171 | >>> from anytree import Node, RenderTree 172 | >>> root = Node("root", lines=["c0fe", "c0de"]) 173 | >>> s0 = Node("sub0", parent=root, lines=["ha", "ba"]) 174 | >>> s0b = Node("sub0B", parent=s0, lines=["1", "2", "3"]) 175 | >>> s0a = Node("sub0A", parent=s0, lines=["a", "b"]) 176 | >>> s1 = Node("sub1", parent=root, lines=["Z"]) 177 | 178 | Simple one line: 179 | 180 | >>> for pre, _, node in RenderTree(root): 181 | ... print("%s%s" % (pre, node.name)) 182 | root 183 | ├── sub0 184 | │ ├── sub0B 185 | │ └── sub0A 186 | └── sub1 187 | 188 | Multiline: 189 | 190 | >>> for pre, fill, node in RenderTree(root): 191 | ... print("%s%s" % (pre, node.lines[0])) 192 | ... for line in node.lines[1:]: 193 | ... print("%s%s" % (fill, line)) 194 | c0fe 195 | c0de 196 | ├── ha 197 | │ ba 198 | │ ├── 1 199 | │ │ 2 200 | │ │ 3 201 | │ └── a 202 | │ b 203 | └── Z 204 | 205 | The `childiter` is responsible for iterating over child nodes at the 206 | same level. An reversed order can be achived by using `reversed`. 207 | 208 | >>> for pre, _, node in RenderTree(root, childiter=reversed): 209 | ... print("%s%s" % (pre, node.name)) 210 | root 211 | ├── sub1 212 | └── sub0 213 | ├── sub0A 214 | └── sub0B 215 | 216 | Or writing your own sort function: 217 | 218 | >>> def mysort(items): 219 | ... return sorted(items, key=lambda item: item.name) 220 | >>> for pre, _, node in RenderTree(root, childiter=mysort): 221 | ... print("%s%s" % (pre, node.name)) 222 | root 223 | ├── sub0 224 | │ ├── sub0A 225 | │ └── sub0B 226 | └── sub1 227 | 228 | :any:`by_attr` simplifies attribute rendering and supports multiline: 229 | 230 | >>> print(RenderTree(root).by_attr()) 231 | root 232 | ├── sub0 233 | │ ├── sub0B 234 | │ └── sub0A 235 | └── sub1 236 | >>> print(RenderTree(root).by_attr("lines")) 237 | c0fe 238 | c0de 239 | ├── ha 240 | │ ba 241 | │ ├── 1 242 | │ │ 2 243 | │ │ 3 244 | │ └── a 245 | │ b 246 | └── Z 247 | """ 248 | if not isinstance(style, AbstractStyle): 249 | style = style() 250 | self.node = node 251 | self.style = style 252 | self.childiter = childiter 253 | 254 | def __iter__(self): 255 | return self.__next(self.node, tuple()) 256 | 257 | def __next(self, node, continues): 258 | yield RenderTree.__item(node, continues, self.style) 259 | children = node.children 260 | if children: 261 | lastidx = len(children) - 1 262 | for idx, child in enumerate(self.childiter(children)): 263 | for grandchild in self.__next(child, continues + (idx != lastidx, )): 264 | yield grandchild 265 | 266 | @staticmethod 267 | def __item(node, continues, style): 268 | if not continues: 269 | return u'', u'', node 270 | else: 271 | items = [style.vertical if cont else style.empty for cont in continues] 272 | indent = ''.join(items[:-1]) 273 | branch = style.cont if continues[-1] else style.end 274 | pre = indent + branch 275 | fill = ''.join(items) 276 | return pre, fill, node 277 | 278 | def __str__(self): 279 | lines = ["%s%r" % (pre, node) for pre, _, node in self] 280 | return "\n".join(lines) 281 | 282 | def __repr__(self): 283 | classname = self.__class__.__name__ 284 | args = [repr(self.node), 285 | "style=%s" % repr(self.style), 286 | "childiter=%s" % repr(self.childiter)] 287 | return "%s(%s)" % (classname, ", ".join(args)) 288 | 289 | def by_attr(self, attrname="name"): 290 | """Return rendered tree with node attribute `attrname`.""" 291 | def get(): 292 | for pre, fill, node in self: 293 | attr = getattr(node, attrname, "") 294 | if isinstance(attr, (list, tuple)): 295 | lines = attr 296 | else: 297 | lines = str(attr).split("\n") 298 | yield u"%s%s" % (pre, lines[0]) 299 | for line in lines[1:]: 300 | yield u"%s%s" % (fill, line) 301 | return "\n".join(get()) 302 | -------------------------------------------------------------------------------- /lib/anytree/resolver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import print_function 5 | 6 | import re 7 | 8 | _MAXCACHE = 20 9 | 10 | 11 | class Resolver(object): 12 | 13 | _match_cache = {} 14 | 15 | def __init__(self, pathattr='name'): 16 | """Resolve :any:`NodeMixin` paths using attribute `pathattr`.""" 17 | super(Resolver, self).__init__() 18 | self.pathattr = pathattr 19 | 20 | def get(self, node, path): 21 | """ 22 | Return instance at `path`. 23 | 24 | An example module tree: 25 | 26 | >>> from anytree import Node 27 | >>> top = Node("top", parent=None) 28 | >>> sub0 = Node("sub0", parent=top) 29 | >>> sub0sub0 = Node("sub0sub0", parent=sub0) 30 | >>> sub0sub1 = Node("sub0sub1", parent=sub0) 31 | >>> sub1 = Node("sub1", parent=top) 32 | 33 | A resolver using the `name` attribute: 34 | 35 | >>> r = Resolver('name') 36 | 37 | Relative paths: 38 | 39 | >>> r.get(top, "sub0/sub0sub0") 40 | Node('/top/sub0/sub0sub0') 41 | >>> r.get(sub1, "..") 42 | Node('/top') 43 | >>> r.get(sub1, "../sub0/sub0sub1") 44 | Node('/top/sub0/sub0sub1') 45 | >>> r.get(sub1, ".") 46 | Node('/top/sub1') 47 | >>> r.get(sub1, "") 48 | Node('/top/sub1') 49 | >>> r.get(top, "sub2") 50 | Traceback (most recent call last): 51 | ... 52 | anytree.resolver.ChildResolverError: Node('/top') has no child sub2. Children are: 'sub0', 'sub1'. 53 | 54 | Absolute paths: 55 | 56 | >>> r.get(sub0sub0, "/top") 57 | Node('/top') 58 | >>> r.get(sub0sub0, "/top/sub0") 59 | Node('/top/sub0') 60 | >>> r.get(sub0sub0, "/") 61 | Traceback (most recent call last): 62 | ... 63 | anytree.resolver.ResolverError: root node missing. root is '/top'. 64 | >>> r.get(sub0sub0, "/bar") 65 | Traceback (most recent call last): 66 | ... 67 | anytree.resolver.ResolverError: unknown root node '/bar'. root is '/top'. 68 | """ 69 | node, parts = self.__start(node, path) 70 | for part in parts: 71 | if part == "..": 72 | node = node.parent 73 | elif part in ("", "."): 74 | pass 75 | else: 76 | node = self.__get(node, part) 77 | return node 78 | 79 | def __get(self, node, name): 80 | for child in node.children: 81 | if _getattr(child, self.pathattr) == name: 82 | return child 83 | raise ChildResolverError(node, name, self.pathattr) 84 | 85 | def glob(self, node, path): 86 | """ 87 | Return instances at `path` supporting wildcards. 88 | 89 | Behaves identical to :any:`get`, but accepts wildcards and returns 90 | a list of found nodes. 91 | 92 | * `*` matches any characters, except '/'. 93 | * `?` matches a single character, except '/'. 94 | 95 | An example module tree: 96 | 97 | >>> from anytree import Node 98 | >>> top = Node("top", parent=None) 99 | >>> sub0 = Node("sub0", parent=top) 100 | >>> sub0sub0 = Node("sub0", parent=sub0) 101 | >>> sub0sub1 = Node("sub1", parent=sub0) 102 | >>> sub1 = Node("sub1", parent=top) 103 | >>> sub1sub0 = Node("sub0", parent=sub1) 104 | 105 | A resolver using the `name` attribute: 106 | 107 | >>> r = Resolver('name') 108 | 109 | Relative paths: 110 | 111 | >>> r.glob(top, "sub0/sub?") 112 | [Node('/top/sub0/sub0'), Node('/top/sub0/sub1')] 113 | >>> r.glob(sub1, ".././*") 114 | [Node('/top/sub0'), Node('/top/sub1')] 115 | >>> r.glob(top, "*/*") 116 | [Node('/top/sub0/sub0'), Node('/top/sub0/sub1'), Node('/top/sub1/sub0')] 117 | >>> r.glob(top, "*/sub0") 118 | [Node('/top/sub0/sub0'), Node('/top/sub1/sub0')] 119 | >>> r.glob(top, "sub1/sub1") 120 | Traceback (most recent call last): 121 | ... 122 | anytree.resolver.ChildResolverError: Node('/top/sub1') has no child sub1. Children are: 'sub0'. 123 | 124 | Non-matching wildcards are no error: 125 | 126 | >>> r.glob(top, "bar*") 127 | [] 128 | >>> r.glob(top, "sub2") 129 | Traceback (most recent call last): 130 | ... 131 | anytree.resolver.ChildResolverError: Node('/top') has no child sub2. Children are: 'sub0', 'sub1'. 132 | 133 | Absolute paths: 134 | 135 | >>> r.glob(sub0sub0, "/top/*") 136 | [Node('/top/sub0'), Node('/top/sub1')] 137 | >>> r.glob(sub0sub0, "/") 138 | Traceback (most recent call last): 139 | ... 140 | anytree.resolver.ResolverError: root node missing. root is '/top'. 141 | >>> r.glob(sub0sub0, "/bar") 142 | Traceback (most recent call last): 143 | ... 144 | anytree.resolver.ResolverError: unknown root node '/bar'. root is '/top'. 145 | """ 146 | node, parts = self.__start(node, path) 147 | return self.__glob(node, parts) 148 | 149 | def __start(self, node, path): 150 | sep = node.separator 151 | parts = path.split(sep) 152 | if path.startswith(sep): 153 | node = node.root 154 | rootpart = _getattr(node, self.pathattr) 155 | parts.pop(0) 156 | if not parts[0]: 157 | msg = "root node missing. root is '%s%s'." 158 | raise ResolverError(node, "", msg % (sep, str(rootpart))) 159 | elif parts[0] != rootpart: 160 | msg = "unknown root node '%s%s'. root is '%s%s'." 161 | raise ResolverError(node, "", msg % (sep, parts[0], sep, str(rootpart))) 162 | parts.pop(0) 163 | return node, parts 164 | 165 | def __glob(self, node, parts): 166 | nodes = [] 167 | name = parts[0] 168 | remainder = parts[1:] 169 | # handle relative 170 | if name == "..": 171 | nodes += self.__glob(node.parent, remainder) 172 | elif name in ("", "."): 173 | nodes += self.__glob(node, remainder) 174 | else: 175 | matches = self.__find(node, name, remainder) 176 | if not matches and not Resolver.is_wildcard(name): 177 | raise ChildResolverError(node, name, self.pathattr) 178 | nodes += matches 179 | return nodes 180 | 181 | def __find(self, node, pat, remainder): 182 | matches = [] 183 | for child in node.children: 184 | name = _getattr(child, self.pathattr) 185 | try: 186 | if Resolver.__match(name, pat): 187 | if remainder: 188 | matches += self.__glob(child, remainder) 189 | else: 190 | matches.append(child) 191 | except ResolverError as exc: 192 | if not Resolver.is_wildcard(pat): 193 | raise exc 194 | return matches 195 | 196 | @staticmethod 197 | def is_wildcard(path): 198 | """Return `True` is a wildcard.""" 199 | return "?" in path or "*" in path 200 | 201 | @staticmethod 202 | def __match(name, pat): 203 | try: 204 | re_pat = Resolver._match_cache[pat] 205 | except KeyError: 206 | res = Resolver.__translate(pat) 207 | if len(Resolver._match_cache) >= _MAXCACHE: 208 | Resolver._match_cache.clear() 209 | Resolver._match_cache[pat] = re_pat = re.compile(res) 210 | return re_pat.match(name) is not None 211 | 212 | @staticmethod 213 | def __translate(pat): 214 | re_pat = '' 215 | for char in pat: 216 | if char == "*": 217 | re_pat += ".*" 218 | elif char == "?": 219 | re_pat += "." 220 | else: 221 | re_pat += re.escape(char) 222 | return re_pat + '\Z(?ms)' 223 | 224 | 225 | class ResolverError(RuntimeError): 226 | 227 | def __init__(self, node, child, msg): 228 | """Resolve Error at `node` handling `child`.""" 229 | super(ResolverError, self).__init__(msg) 230 | self.node = node 231 | self.child = child 232 | 233 | 234 | class ChildResolverError(ResolverError): 235 | 236 | def __init__(self, node, child, pathattr): 237 | """Child Resolve Error at `node` handling `child`.""" 238 | names = [repr(_getattr(c, pathattr)) for c in node.children] 239 | msg = "%r has no child %s. Children are: %s." 240 | msg = msg % (node, child, ", ".join(names)) 241 | super(ChildResolverError, self).__init__(node, child, msg) 242 | 243 | 244 | def _getattr(node, name): 245 | return getattr(node, name, None) 246 | -------------------------------------------------------------------------------- /lib/anytree/walker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Walker(object): 5 | 6 | def __init__(self): 7 | """Walk from one node to another.""" 8 | super(Walker, self).__init__() 9 | 10 | def walk(self, start, end): 11 | """ 12 | Walk from `start` node to `end` node. 13 | 14 | Returns: 15 | (upwards, common, downwards): `upwards` is a list of nodes to go upward to. 16 | `common` top node. `downwards` is a list of nodes to go downward to. 17 | 18 | Raises: 19 | WalkError: on no common root node. 20 | 21 | >>> from anytree import Node, RenderTree, AsciiStyle 22 | >>> f = Node("f") 23 | >>> b = Node("b", parent=f) 24 | >>> a = Node("a", parent=b) 25 | >>> d = Node("d", parent=b) 26 | >>> c = Node("c", parent=d) 27 | >>> e = Node("e", parent=d) 28 | >>> g = Node("g", parent=f) 29 | >>> i = Node("i", parent=g) 30 | >>> h = Node("h", parent=i) 31 | >>> print(RenderTree(f, style=AsciiStyle())) 32 | Node('/f') 33 | |-- Node('/f/b') 34 | | |-- Node('/f/b/a') 35 | | +-- Node('/f/b/d') 36 | | |-- Node('/f/b/d/c') 37 | | +-- Node('/f/b/d/e') 38 | +-- Node('/f/g') 39 | +-- Node('/f/g/i') 40 | +-- Node('/f/g/i/h') 41 | 42 | Create a walker: 43 | 44 | >>> w = Walker() 45 | 46 | This class is made for walking: 47 | 48 | >>> w.walk(f, f) 49 | ((), Node('/f'), ()) 50 | >>> w.walk(f, b) 51 | ((), Node('/f'), (Node('/f/b'),)) 52 | >>> w.walk(b, f) 53 | ((Node('/f/b'),), Node('/f'), ()) 54 | >>> w.walk(h, e) 55 | ((Node('/f/g/i/h'), Node('/f/g/i'), Node('/f/g')), Node('/f'), (Node('/f/b'), Node('/f/b/d'), Node('/f/b/d/e'))) 56 | >>> w.walk(d, e) 57 | ((), Node('/f/b/d'), (Node('/f/b/d/e'),)) 58 | 59 | For a proper walking the nodes need to be part of the same tree: 60 | 61 | >>> w.walk(Node("a"), Node("b")) 62 | Traceback (most recent call last): 63 | ... 64 | anytree.walker.WalkError: Node('/a') and Node('/b') are not part of the same tree. 65 | """ 66 | s = start.path 67 | e = end.path 68 | if start.root != end.root: 69 | msg = "%r and %r are not part of the same tree." % (start, end) 70 | raise WalkError(msg) 71 | # common 72 | c = Walker.__calc_common(s, e) 73 | assert c[0] is start.root 74 | len_c = len(c) 75 | # up 76 | if start is c[-1]: 77 | up = tuple() 78 | else: 79 | up = tuple(reversed(s[len_c:])) 80 | # down 81 | if end is c[-1]: 82 | down = tuple() 83 | else: 84 | down = e[len_c:] 85 | return up, c[-1], down 86 | 87 | @staticmethod 88 | def __calc_common(s, e): 89 | return tuple([si for si, ei in zip(s, e) if si is ei]) 90 | 91 | 92 | class WalkError(RuntimeError): 93 | 94 | """Walk Error.""" 95 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from Chromosome import Chromosome 4 | from GeneExpressionProgram import GeneExpressionProgram 5 | 6 | import numpy as np 7 | 8 | 9 | def a4_a3_a2_a1(): 10 | 11 | # define objective function 12 | GeneExpressionProgram.OBJECTIVE_FUNCTION = staticmethod(lambda a: a ** 4 + a ** 3 + a ** 2 + a) 13 | GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX = 0, 20 14 | 15 | # Define terminals and functions 16 | Chromosome.terminals = ["a"] 17 | Chromosome.functions = { 18 | "+": {"args": 2, "f": lambda x, y: x + y}, 19 | "-": {"args": 2, "f": lambda x, y: x - y}, 20 | "*": {"args": 2, "f": lambda x, y: x * y}, 21 | "/": {"args": 2, "f": lambda x, y: x / y} 22 | } 23 | Chromosome.fitness_cases = [ 24 | ({"a": a}, GeneExpressionProgram.OBJECTIVE_FUNCTION(a)) 25 | for a in np.random.rand(GeneExpressionProgram.NUM_FITNESS_CASES) * ( 26 | GeneExpressionProgram.OBJECTIVE_MAX - GeneExpressionProgram.OBJECTIVE_MIN) + GeneExpressionProgram.OBJECTIVE_MIN 27 | ] 28 | 29 | GeneExpressionProgram.FUNCTION_Y_RANGE = \ 30 | GeneExpressionProgram.OBJECTIVE_FUNCTION(GeneExpressionProgram.OBJECTIVE_MAX) - \ 31 | GeneExpressionProgram.OBJECTIVE_FUNCTION(GeneExpressionProgram.OBJECTIVE_MIN) 32 | 33 | Chromosome.linking_function = "+" 34 | Chromosome.max_fitness = 1 #GeneExpressionProgram.FUNCTION_Y_RANGE * GeneExpressionProgram.NUM_FITNESS_CASES 35 | 36 | ans = GeneExpressionProgram.evolve() 37 | ans.print_tree() 38 | 39 | 40 | def hard_regression(): 41 | # TODO - crossover and mutation of constants 42 | # define objective function 43 | GeneExpressionProgram.OBJECTIVE_FUNCTION = staticmethod(lambda a: 4.251*np.power(a, 2) + np.log(np.power(a, 2)) + 7.243*np.exp(a)) 44 | GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX = -1, 1 45 | GeneExpressionProgram.NUM_FITNESS_CASES = 20 46 | GeneExpressionProgram.FITNESS_FUNCTION = Chromosome.inv_squared_error 47 | GeneExpressionProgram.NUM_GENERATIONS = 5000 48 | GeneExpressionProgram.NUM_RUNS = 5 49 | GeneExpressionProgram.MUTATION_RATE = 0.041 50 | 51 | # Define terminals and functions 52 | Chromosome.terminals = ["a", "?"] 53 | Chromosome.ephemeral_random_constants_range = (-1, 1) 54 | Chromosome.functions = { 55 | "+": {"args": 2, "f": lambda x, y: x + y}, 56 | "-": {"args": 2, "f": lambda x, y: x - y}, 57 | "*": {"args": 2, "f": lambda x, y: x * y}, 58 | "/": {"args": 2, "f": lambda x, y: x / y if y != 0 else np.nan}, 59 | "L": {"args": 1, "f": lambda x: np.log(x) if x > 0 else np.nan}, 60 | "E": {"args": 1, "f": lambda x: np.exp(x)}, 61 | #"K": {"args": 1, "f": lambda x: np.log10(x) if x > 0 else np.nan}, 62 | #"~": {"args": 1, "f": lambda x: np.power(10, x)}, 63 | #"S": {"args": 1, "f": lambda x: np.sin(x)}, 64 | #"C": {"args": 1, "f": lambda x: np.cos(x)} 65 | } 66 | Chromosome.fitness_cases = [ 67 | ({"a": a}, GeneExpressionProgram.OBJECTIVE_FUNCTION(a)) 68 | for a in np.random.rand(GeneExpressionProgram.NUM_FITNESS_CASES) * ( 69 | GeneExpressionProgram.OBJECTIVE_MAX - GeneExpressionProgram.OBJECTIVE_MIN) + GeneExpressionProgram.OBJECTIVE_MIN 70 | ] 71 | 72 | Chromosome.linking_function = "+" 73 | Chromosome.length = 60 74 | Chromosome.head_length = 20 75 | Chromosome.max_fitness = 1 #GeneExpressionProgram.FUNCTION_Y_RANGE * GeneExpressionProgram.NUM_FITNESS_CASES 76 | 77 | average_fitnesses = [] 78 | best_fitnesses = [] 79 | answers = [] 80 | 81 | ans = None 82 | for rep in range(GeneExpressionProgram.NUM_RUNS): 83 | print("====================================== Rep %d ======================================" % rep) 84 | ans, gen_average_fitnesses, gen_best_fitnesses = GeneExpressionProgram.evolve() 85 | average_fitnesses.append(gen_average_fitnesses) 86 | best_fitnesses.append(gen_best_fitnesses) 87 | ans.print_tree() 88 | print(ans.genes) 89 | answers.append(deepcopy(ans)) 90 | 91 | print("====================================== Random Search ======================================") 92 | random_search_individual, random_search_avg, random_search_best = GeneExpressionProgram.random_search( 93 | GeneExpressionProgram.NUM_GENERATIONS, 94 | GeneExpressionProgram.FITNESS_FUNCTION, 95 | GeneExpressionProgram.FITNESS_FUNCTION_ARGS 96 | ) 97 | 98 | # Good genes: 99 | #ans = Chromosome(['+/aE?aa?aaEa*Ea+E/?L?aa??aaaaa???aaaaa???aa?a??a?aa???aaa??a?a??a?aaaaaa?aaa??aa', '?a???L*Laa+++L-?///aaa??????a?aa?aaa?aaa?aaa?aa??a?a?a?aa??aaa?a?aaaaaa?aa???aaa', '+L**E*aaEaa+E??*/a*-???a?aa?aaaa?aaaaaaa?a?aaaa?aa?aaaaaaa???aa?aaaa?aa??aaa????']) 100 | #ans.ephemeral_random_constants = [0.15711028370174618, 0.8582913293794245, 0.08834476189713181, -0.6507871955790874, -0.5001482762755984, -0.36339831251011967, 0.49239731282122023, -0.7707384044151064, 0.33957437653782097, -0.050909069821311936, 0.40688394042469067, 0.8838615430620207, -0.03950637280673064, -0.8398663459127116, -0.9701111175669401, 0.16516078130630563, -0.5163031755060181, 0.26930803528455916, -0.7833159749333989, -0.7075160969083776, -0.6751546227334948, 0.1505636368911023, -0.16805240822390255, -0.34424168190370485, -0.8544980338428079, -0.01217703484745547, -0.24005751860391533, -0.5077198074421936, -0.47443544577074226, 0.5247967085947582, -0.22543318048008576, 0.4938865002308659, -0.6465093618701077, -0.19098460727467326, -0.2944062401077585, 0.7016839377380519, -0.14341637591100542, 0.23227088210671476, 0.36051772215302, 0.6509343605188611, -0.332150327502315, 0.3171544096208032, 0.2676850827993431, -0.46506262502073414, 0.843996276413379, -0.5614960005334659, 0.47891344757490373, -0.5575325624206526, -0.8156525364269045, -0.31652263727243746, 0.06884531540832572, 0.8836222097723032, 0.6601557412383419, -0.7869161076217597, -0.16110865846423783, -0.06702662866877018, -0.22568995470545228, 0.9438977429740325, -0.6410133183089668, 0.045353882523733624, 0.16786587502585948, -0.9008872395302681, 0.004355424045595413, 0.9179463218466064, 0.4105708444235643, -0.001799675111191501, -0.4201794697378056, -0.37672122028632216, 0.5906938113771141, 0.6004718433032417, -0.4698494772153414, 0.04238505345795085, 0.1657146428146341, 0.5148585050576182, 0.6955837854892111, -0.08812485635975653, -0.4101965485774235, 0.7234363214796748, 0.14285945798729927, 0.6450352601522822] 101 | #ans.plot_solution(GeneExpressionProgram.OBJECTIVE_FUNCTION, GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX, [], [], "a") 102 | 103 | #ans = Chromosome([0.10325095527275208, -0.6353207023355358, 0.3087002846077995, -0.646063772861873, -0.7604999404652737, -0.7026365243175745, -0.8546384172276964, -0.9210656698793773, -0.92473371383499159, 0.38475054329790037, 0.011392225394319944, -0.18840805047095377, 0.40103873272978197, 0.06328742279657229, 0.7176441901359412, -0.26941389839428265, 0.8330939983953638, -0.3291581824265808, -0.6081511326274629, 0.9437496769675557, 0.14649019501924365, -0.026586568971478375, 0.6942711369955727, -0.4952751886918654, 0.49348107956565457, 0.6373366861063929, -0.4180513467203788, 0.37222546291909575, 0.09407582333744235, -0.7976697480943422, -0.04875126785507633, 0.0738340898048051, 0.4574594498229325, 0.6176526203069368, -0.8316660105318336, 0.328772037314927, 0.854958551073552, -0.3935592840377773, -0.1523385500807699, -0.8272148377421589, 0.8580710774374207, -0.18672644199209687, 0.5393722078778671, -0.6906345459192962, -0.22443846331305117, 0.34512085908760115, 0.21516058072431332, -0.8499353974445536, 0.26989917691422316, -0.44201601204751584, 0.00887253706744362, -0.7836088648008288, -0.8679234491746306, -0.07209104779770104, 0.16000238129327515, 0.8890857194001152, -0.6325595149575627, -0.11804832230841544, -0.018168330413408817, -0.6561487673112327, 0.18667288624065526, -0.21171583904469493, -0.7074160949096726, -0.5647179047402893, 0.6466522462832285, 0.17157152187819658, -0.55051406791476754, -0.32459344245456756, -0.73568042741430184, 0.39543504084587644, -0.9332865781958235, 0.9903268434472272, 0.05041449572068979, 0.0798497529910911, -0.66617358746171074, 0.5071192429292173, 0.38014218205802264, -0.929868126429702, -0.23615736970440504, 0.5939936181262968]) 104 | #ans.ephemeral_random_constants = ['+EEEaa/L-+L-EL-L?aLEaa??????aa?aa?????aa??a???aaaaaaa????aa?aa??aa??a?a??aaa?aaa', '+EEE*aa?+?*/LLa/*+*-a?a?aaaa??a?aa?a??aa???aaaaa????a???????a?aaa????????aaaaaa?', '-L***aaa--+*+*+?+?--?aa??aa?aa?aa??a??a?aa??aaaaa?a??a?aa?aa?????aaaa??aa?aaa?a?'] 105 | 106 | best = max(answers, key=lambda c:c.fitness()) 107 | ans.plot_solution(GeneExpressionProgram.OBJECTIVE_FUNCTION, GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX, average_fitnesses[0], best_fitnesses[0], "a") 108 | GeneExpressionProgram.plot_reps(average_fitnesses, best_fitnesses, random_search_avg, random_search_best) 109 | 110 | 111 | def sinx_polynomial(): 112 | # define objective function 113 | GeneExpressionProgram.OBJECTIVE_FUNCTION = staticmethod(lambda a: np.sin(a)) 114 | GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX = -5, 5 115 | GeneExpressionProgram.FITNESS_FUNCTION = Chromosome.centralized_inv_squared_error 116 | GeneExpressionProgram.FITNESS_FUNCTION_ARGS = [0, "x"] 117 | GeneExpressionProgram.NUM_FITNESS_CASES = 10 118 | GeneExpressionProgram.NUM_GENERATIONS = 150 119 | GeneExpressionProgram.POPULATION_SIZE = 200 120 | GeneExpressionProgram.MUTATION_RATE = 0.05 121 | 122 | Chromosome.head_length = 8 123 | Chromosome.num_genes = 3 124 | Chromosome.length = 24 125 | 126 | # Define terminals and functions 127 | Chromosome.terminals = ["x", "6", "!"] 128 | Chromosome.constants = {"!": 120} 129 | Chromosome.functions = { 130 | "+": {"args": 2, "f": lambda x, y: x + y}, 131 | "-": {"args": 2, "f": lambda x, y: x - y}, 132 | "*": {"args": 2, "f": lambda x, y: x * y}, 133 | "/": {"args": 2, "f": lambda x, y: x / y} 134 | #"^": {"args": 2, "f": lambda x, y: x ** y} 135 | } 136 | Chromosome.fitness_cases = [ 137 | ({"x": x}, GeneExpressionProgram.OBJECTIVE_FUNCTION(x)) 138 | for x in np.random.rand(GeneExpressionProgram.NUM_FITNESS_CASES) * ( 139 | GeneExpressionProgram.OBJECTIVE_MAX - GeneExpressionProgram.OBJECTIVE_MIN) + GeneExpressionProgram.OBJECTIVE_MIN 140 | ] 141 | 142 | GeneExpressionProgram.FUNCTION_Y_RANGE = 2 143 | Chromosome.linking_function = "*" 144 | Chromosome.max_fitness = 1 #GeneExpressionProgram.FUNCTION_Y_RANGE * GeneExpressionProgram.NUM_FITNESS_CASES 145 | 146 | ans, average_fitnesses, best_fitnesses = GeneExpressionProgram.evolve() 147 | ans.print_tree() 148 | ans.plot_solution(GeneExpressionProgram.OBJECTIVE_FUNCTION, 149 | GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX, 150 | average_fitnesses, best_fitnesses) 151 | 152 | 153 | def euclidean_distance(): 154 | 155 | def random_fitness_case(f, function_min: float, function_max: float) -> (dict, float): 156 | x = np.random.rand() * (function_max - function_min) + function_min 157 | y = np.random.rand() * (function_max - function_min) + function_min 158 | key = {"x": x, "y": y} 159 | value = f(x, y) 160 | return key, value 161 | 162 | # define objective function 163 | GeneExpressionProgram.OBJECTIVE_FUNCTION = staticmethod(lambda x, y: (x**2 + y**2)**0.5) 164 | GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX = 0, 20 165 | 166 | # Define terminals and functions 167 | Chromosome.terminals = ["x", "y"] 168 | Chromosome.functions = { 169 | "+": {"args": 2, "f": lambda x, y: x + y}, 170 | "-": {"args": 2, "f": lambda x, y: x - y}, 171 | "*": {"args": 2, "f": lambda x, y: x * y}, 172 | "/": {"args": 2, "f": lambda x, y: x / y}, 173 | "Q": {"args": 1, "f": lambda x: x**0.5} 174 | } 175 | 176 | Chromosome.fitness_cases = [ 177 | random_fitness_case(GeneExpressionProgram.OBJECTIVE_FUNCTION, GeneExpressionProgram.OBJECTIVE_MIN, GeneExpressionProgram.OBJECTIVE_MAX) 178 | for _ in range(GeneExpressionProgram.NUM_FITNESS_CASES) 179 | ] 180 | 181 | Chromosome.linking_function = "+" 182 | Chromosome.num_genes = 3 183 | Chromosome.length = 39 184 | Chromosome.head_length = 10 185 | Chromosome.max_fitness = 1 186 | 187 | GeneExpressionProgram.FUNCTION_Y_RANGE = GeneExpressionProgram.OBJECTIVE_FUNCTION(GeneExpressionProgram.OBJECTIVE_MAX, GeneExpressionProgram.OBJECTIVE_MAX) 188 | 189 | ans = GeneExpressionProgram.evolve() 190 | ans.print_tree() 191 | 192 | 193 | def cart_pole_bool(): 194 | 195 | import gym 196 | env = gym.make('CartPole-v1') 197 | 198 | def fitness(num_trials: int, *args) -> np.ndarray: 199 | fitnesses = [] 200 | for chromosome_index in range(len(args)): 201 | chromosome = args[chromosome_index] 202 | total_reward = 0 203 | #print("\tCalculating chromosome:\t%d" % chromosome_index, end="\t") 204 | for trial in range(num_trials): 205 | x, theta, dx, dtheta = env.reset() 206 | t = 0 207 | while True: 208 | #env.render() 209 | action = chromosome.evaluate({"x": x > 0, "v": dx > 0, "t": theta > 0, "u": dtheta > 0}) 210 | #action = chromosome.evaluate({"v": dx > 0, "t": theta > 0, "u": dtheta > 0}) 211 | observation, reward, done, info = env.step(action) 212 | x, theta, dx, dtheta = observation 213 | total_reward += reward 214 | if done: 215 | break 216 | t += 1 217 | chromosome._fitness_ = total_reward / float(num_trials) 218 | fitnesses.append(total_reward / float(num_trials)) 219 | #print("Fitness: %.5f" % (total_reward / float(num_trials))) 220 | return np.asarray(fitnesses) 221 | 222 | Chromosome.functions = { 223 | 224 | # if function 225 | "?": {"args": 3, "f": lambda x, y, z: y if x else z}, 226 | 227 | # and/or functions 228 | "&": {"args": 2, "f": lambda x, y: x and y}, 229 | "|": {"args": 2, "f": lambda x, y: x or y}, 230 | 231 | # not function 232 | "!": {"args": 1, "f": lambda x: not x}, 233 | 234 | # xor/nor functions 235 | "^": {"args": 2, "f": lambda x, y: (x and not y) or (y and not x)}, 236 | "#": {"args": 2, "f": lambda x, y: not (x and y)} 237 | } 238 | 239 | Chromosome.terminals = [ 240 | "x", # cart x-value 241 | "v", # cart velocity 242 | "t", # pole angle 243 | "u" # rate of change of pole angle 244 | ] 245 | 246 | GeneExpressionProgram.FITNESS_FUNCTION = fitness 247 | GeneExpressionProgram.FITNESS_FUNCTION_ARGS = [10] 248 | GeneExpressionProgram.POPULATION_SIZE = 10 249 | GeneExpressionProgram.ERROR_TOLERANCE = 5 250 | 251 | Chromosome.max_fitness = 500 252 | Chromosome.num_genes = 3 253 | Chromosome.length = 45 254 | Chromosome.head_length = 15 255 | Chromosome.linking_function = "&" 256 | 257 | ans, average_fitnesses, best_fitnesses = GeneExpressionProgram.evolve() 258 | ans.print_tree() 259 | ans.plot_solution(None, 0, 0, average_fitnesses, best_fitnesses) 260 | print(ans.genes) 261 | 262 | 263 | def cart_pole_real(): 264 | 265 | import gym 266 | env = gym.make('CartPole-v1') 267 | 268 | def fitness(num_trials: int, render: bool = False, doPrint: bool = False, *args) -> np.ndarray: 269 | fitnesses = [] 270 | for chromosome_index in range(len(args)): 271 | chromosome = args[chromosome_index] 272 | total_reward = 0 273 | if doPrint: 274 | print("\tCalculating chromosome %d across %d trials." % (chromosome_index, num_trials), end="\t") 275 | for trial in range(num_trials): 276 | x, theta, dx, dtheta = env.reset() 277 | t = 0 278 | while True: 279 | if render: env.render() 280 | action = chromosome.evaluate({"x": x, "v": dx, "t": theta, "u": dtheta}) 281 | observation, reward, done, info = env.step(action > 0) 282 | x, theta, dx, dtheta = observation 283 | total_reward += reward 284 | if done: 285 | break 286 | t += 1 287 | chromosome._fitness_ = total_reward / float(num_trials) 288 | fitnesses.append(total_reward / float(num_trials)) 289 | if doPrint: 290 | print("Fitness: %.5f" % (total_reward / float(num_trials))) 291 | return np.asarray(fitnesses) 292 | 293 | Chromosome.functions = { 294 | "+": {"args": 2, "f": lambda x, y: x + y}, 295 | "-": {"args": 2, "f": lambda x, y: x - y}, 296 | "*": {"args": 2, "f": lambda x, y: x * y}, 297 | "/": {"args": 2, "f": lambda x, y: x / y if y != 0 else np.inf} 298 | } 299 | 300 | Chromosome.terminals = [ 301 | "x", # cart x-value 302 | "v", # cart velocity 303 | "t", # pole angle 304 | "u" # rate of change of pole angle 305 | ] 306 | 307 | GeneExpressionProgram.FITNESS_FUNCTION = fitness 308 | GeneExpressionProgram.FITNESS_FUNCTION_ARGS = [10, False, False] 309 | GeneExpressionProgram.POPULATION_SIZE = 10 310 | GeneExpressionProgram.ERROR_TOLERANCE = 0 311 | GeneExpressionProgram.NUM_RUNS = 5 312 | GeneExpressionProgram.NUM_GENERATIONS = 15 313 | GeneExpressionProgram.MUTATION_RATE = 0.05 314 | 315 | Chromosome.max_fitness = 500 316 | Chromosome.num_genes = 3 317 | Chromosome.length = 30 318 | Chromosome.head_length = 10 319 | Chromosome.linking_function = "+" 320 | 321 | average_fitnesses = [] 322 | best_fitnesses = [] 323 | 324 | ans = None 325 | for rep in range(GeneExpressionProgram.NUM_RUNS): 326 | print("====================================== Rep %d ======================================" % rep) 327 | ans, gen_average_fitnesses, gen_best_fitnesses = GeneExpressionProgram.evolve() 328 | average_fitnesses.append(gen_average_fitnesses) 329 | best_fitnesses.append(gen_best_fitnesses) 330 | #ans.print_tree() 331 | #print(ans.genes) 332 | 333 | print("====================================== Random Search ======================================") 334 | random_search_individual, random_search_avg, random_search_best = GeneExpressionProgram.random_search( 335 | GeneExpressionProgram.NUM_GENERATIONS, 336 | GeneExpressionProgram.FITNESS_FUNCTION, 337 | GeneExpressionProgram.FITNESS_FUNCTION_ARGS 338 | ) 339 | 340 | GeneExpressionProgram.plot_reps(average_fitnesses, best_fitnesses, random_search_avg, random_search_best) 341 | 342 | fitness(100, True, True, ans) 343 | 344 | # Very good genes (500 fitness over 100 attempts): 345 | # ['-u-uvx*txtxxutuutvtvtvttttutvu', '+*vxu/x/-/tutxtxxxuvuuxvtuvvtu', '+ut*vv*vtvxvxvuxvtxvtvuutvtttt'] 346 | # ['t/v*/xtttvuxtvvttutxxtuvxuvttt', '-v*+v*+t+tuxtvvttuuvxvtuvuvxtt', 'u++tu+uvtvtxtvtvtuvxxttvxvxvv'] 347 | # ['-t-*+u*u-*tuutxxuutxtxttxttvxu', '+-u**+-/vxvxxxvtxvxuxvxvvxxtvx', '+xv-xvx-xtuutxxtxxxtxxuttvxxut'] 348 | 349 | 350 | if __name__ == "__main__": 351 | 352 | #a4_a3_a2_a1() 353 | #sinx_polynomial() 354 | #cart_pole_real() 355 | hard_regression() 356 | -------------------------------------------------------------------------------- /tests/ChromosomeTests.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | 4 | from Chromosome import Chromosome 5 | 6 | 7 | # noinspection PyMethodMayBeStatic 8 | class ChromosomeTests(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.F = { 12 | "+": {"args": 2, "f": lambda x, y: x + y}, 13 | "-": {"args": 2, "f": lambda x, y: x - y}, 14 | "*": {"args": 2, "f": lambda x, y: x * y}, 15 | "/": {"args": 2, "f": lambda x, y: x / y}, 16 | "Q": {"args": 1, "f": lambda x: x ** 0.5}, 17 | "I": {"args": 3, "f": lambda x, y, z: y if x else z} 18 | } 19 | self.T = ["a", "b", "2", "3", "1", "u", "c"] 20 | self.HEAD_LENGTH = 6 21 | self.objective_function = lambda a: a ** 4 + a ** 3 + a ** 2 + a 22 | Chromosome.functions = self.F 23 | Chromosome.terminals = self.T 24 | Chromosome.head_length = self.HEAD_LENGTH 25 | Chromosome.num_genes = 1 26 | Chromosome.fitness_cases = [(a, self.objective_function(a)) for a in np.random.rand(10) * 20] 27 | 28 | def test_build_tree1(self): 29 | 30 | c1 = Chromosome(["Q*Q+bbaaa"]) 31 | t = Chromosome.build_tree(c1.genes[0]) 32 | assert( 33 | str(t.descendants).replace("\\", "") == 34 | r"(Node(\'/Q/*\'), Node(\'/Q/*/Q\'), Node(\'/Q/*/Q/b\'), Node(\'/Q/*/+\'), Node(\'/Q/*/+/b\'), Node(\'/Q/*/+/a\'))".replace("\\", "") 35 | ) 36 | 37 | def test_build_tree2(self): 38 | c2 = Chromosome(["IIIIIIIIIIIII131u3ab2ubab23c3ua31a333au3"]) 39 | t = Chromosome.build_tree(c2.genes[0]) 40 | assert( 41 | str(t.descendants).replace("\\", "") == 42 | r"(Node('/I/I'), Node('/I/I/I'), Node('/I/I/I/1'), Node('/I/I/I/3'), Node('/I/I/I/1'), Node('/I/I/I'), Node('/I/I/I/u'), Node('/I/I/I/3'), Node('/I/I/I/a'), Node('/I/I/I'), Node('/I/I/I/b'), Node('/I/I/I/2'), Node('/I/I/I/u'), Node('/I/I'), Node('/I/I/I'), Node('/I/I/I/b'), Node('/I/I/I/a'), Node('/I/I/I/b'), Node('/I/I/I'), Node('/I/I/I/2'), Node('/I/I/I/3'), Node('/I/I/I/c'), Node('/I/I/I'), Node('/I/I/I/3'), Node('/I/I/I/u'), Node('/I/I/I/a'), Node('/I/I'), Node('/I/I/I'), Node('/I/I/I/3'), Node('/I/I/I/1'), Node('/I/I/I/a'), Node('/I/I/I'), Node('/I/I/I/3'), Node('/I/I/I/3'), Node('/I/I/I/3'), Node('/I/I/I'), Node('/I/I/I/a'), Node('/I/I/I/u'), Node('/I/I/I/3'))".replace("\\", "") 43 | ) 44 | 45 | def test_build_tree3(self): 46 | c3 = Chromosome(["/a*+b-cbabaccbac"]) 47 | t = Chromosome.build_tree(c3.genes[0]) 48 | assert( 49 | str(t.descendants).replace("\\", "") == 50 | r"(Node(\\'///a\\'), Node(\\'///*\\'), Node(\\'///*/+\\'), Node(\\'///*/+/-\\'), Node(\\'///*/+/-/b\\'), Node(\\'///*/+/-/a\\'), Node(\\'///*/+/c\\'), Node(\\'///*/b\\'))".replace("\\", "") 51 | ) 52 | 53 | def test_evaluate1(self): 54 | c1 = Chromosome(["Q*Q+bbaaa"]) 55 | assert( 56 | c1.evaluate(terminal_values={ 57 | "a": 3, 58 | "b": 1 59 | }) == 2 60 | ) 61 | 62 | def test_evaluate2(self): 63 | Chromosome.linking_function = "+" 64 | Chromosome.num_genes = 2 65 | c1 = Chromosome(["Q*Q+bbaaa", "*-babaabb"]) 66 | assert( 67 | c1.evaluate(terminal_values={ 68 | "a": 3, 69 | "b": 1 70 | }) == 4 71 | ) 72 | 73 | def test_evaluate3(self): 74 | Chromosome.linking_function = "-" 75 | Chromosome.num_genes = 2 76 | c1 = Chromosome(["Q*Q+bbaaa", "*-babaabb"]) 77 | assert ( 78 | c1.evaluate(terminal_values={ 79 | "a": 3, 80 | "b": 1 81 | }) == 0 82 | ) 83 | 84 | 85 | if __name__ == "__main__": 86 | unittest.main() -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeff-dale/Gene-Expression-Programming/0c4dcb1fcf548470414062cd421283c92f19b12b/tests/__init__.py --------------------------------------------------------------------------------