From a36bef150c843efb79eb36b51eeab9ca2abeb4c0 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Thu, 30 Jun 2016 19:18:37 +0200 Subject: [PATCH 01/20] CGP (Cartesian Genetic Programming) Addon Added genome type and initializator for it with tests. --- .gitignore | 2 + pyevolve/Consts.py | 6 +- pyevolve/G2DCartesian.py | 195 +++++++++++++++++++++++++++++++++++ pyevolve/Initializators.py | 75 ++++++++++++++ tests/test_genomes.py | 88 ++++++++++++++++ tests/test_initializators.py | 168 ++++++++++++++++++++++++++++++ 6 files changed, 533 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 pyevolve/G2DCartesian.py create mode 100644 tests/test_genomes.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d81d41e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.coverage +*.pyc \ No newline at end of file diff --git a/pyevolve/Consts.py b/pyevolve/Consts.py index f1442cd..b942f7c 100644 --- a/pyevolve/Consts.py +++ b/pyevolve/Consts.py @@ -373,6 +373,7 @@ import Crossovers import logging from GTree import GTreeGP +from G2DCartesian import G2DCartesian # Required python version 2.5+ CDefPythonRequire = (2, 5) @@ -491,6 +492,9 @@ CDefG2DListInit = Initializators.G2DListInitializatorInteger CDefG2DListCrossUniformProb = 0.5 +# - G2DCartesian defaults +CDefG2DCartesianInit = Initializators.G2DCartesianInitializatorNode + # Gaussian Gradient CDefGaussianGradientMU = 1.0 CDefGaussianGradientSIGMA = (1.0 / 3.0) # approx. +/- 3-sigma is +/- 10% @@ -525,7 +529,7 @@ CDefBroadcastAddress = "255.255.255.255" nodeType = {"TERMINAL": 0, "NONTERMINAL": 1} -CDefGPGenomes = [GTreeGP] +CDefGPGenomes = [GTreeGP, G2DCartesian] # Migration Consts CDefGenMigrationRate = 20 diff --git a/pyevolve/G2DCartesian.py b/pyevolve/G2DCartesian.py new file mode 100644 index 0000000..e60a0f7 --- /dev/null +++ b/pyevolve/G2DCartesian.py @@ -0,0 +1,195 @@ +""" +:mod:`G2DCartesian` -- the 2D cartesian net chromosome +================================================================ +This is the 2D Cartesian Net representation for Cartesian Genetic Programming +(CGP), this net consist of nodes which hold function or terminals (like in +Genetic Programming). +This chromosome class extends the :class:`GenomeBase.GenomeBase`. +Default Parameters +------------------------------------------------------------- +*Initializator* + :func:`Initializators.G2DCartesianInitializatorNode` + The Node Initializator for G2DCartesian +*Mutator* + :func:`None` + The Mutator for G2DCartesian +*Crossover* + :func:`None` + The Crossover for G2DCartesian +Class +------------------------------------------------------------- +""" + +from GenomeBase import GenomeBase +import Consts +import random +import pydot +import copy + +class G2DCartesian(GenomeBase): + + """ G2DCartesian Class - The 2D Cartesian Net chromosome representation + Inheritance diagram for :class:`G2DCartesian.G2DCartesian`: + .. inheritance-diagram:: G2DCartesian.G2DCartesian + **Examples** + The instantiation + >>> genome1 = G2DCartesian.G2DCartesian(2, 3, 4, 2) + Compare + >>> genome2 = genome1.clone() + >>> genome2 == genome1 + True + Size, slice, get/set, append + >>> len(genome) + 12 + >>> genome + (...) + [None, None, None, None, None, None, None, None, None, None, None, + None] + >>> genome[1] = 2 + >>> genome + (...) + [None, 2, None, None, None, None, None, None, None, None, None, None] + >>> genome[1] + 2 + :param rows: the number of rows in the net + :param cols: the number of columns in the net + :param inputs: the number of inputs nodes + :param outputs: the number of output nodes + """ + + __slots__ = ["inputs", "outputs", "cols", "rows", "internalNodes", + "inputSlice", "internalSlice", "outputSlice", "nodes"] + + def __init__(self, rows, cols, inputs, outputs, cloning = False): + + if (rows * cols * inputs * outputs) == 0: + raise ValueError("One of the genome parameter equals 0.") + + super(G2DCartesian, self).__init__() + self.rows = rows + self.cols = cols + self.inputs = inputs + self.outputs = outputs + self.internalNodes = rows*cols + self.nodes = [None]*(rows*cols+outputs+inputs) + self.inputSlice = slice(0,self.inputs) + self.internalSlice = slice(self.inputs, self.inputs+self.internalNodes) + self.outputSlice = slice(self.inputs+self.internalNodes, + self.inputs+self.internalNodes+self.outputs) + + if not cloning: + self.initializator.set(Consts.CDefG2DCartesianInit) + self.mutator.set(Consts.CDefG2DCartesianMutator) + self.crossover.set(Consts.CDefG2DCartesianCrossover) + + def __eq__(self, other): + cond1 = (self.nodes == other.nodes) + cond2 = (self.rows == other.rows) + cond3 = (self.cols == other.cols) + return True if cond1 and cond2 and cond3 else False + + def __getitem__(self, key): + return self.nodes[key] + + def __iter__(self): + return iter(self.nodes) + + def __len__(self): + return len(self.nodes) + + def __repr__(self): + ret = GenomeBase.__repr__(self) + ret += "- G2DCartesian\n" + ret += "\tList size:\t %s\n" % (len(self.nodes,)) + ret += "\tList:\t\t %s\n\n" % (self.nodes,) + return ret + + def __setitem__(self, key, value): + self.nodes[key] = value + + def clone(self): + newcopy = G2DCartesian(self.rows, self.cols, self.inputs, self.outputs, + True) + self.copy(newcopy) + return newcopy + + def copy(self, g): + GenomeBase.copy(self, g) + g.nodes = copy.deepcopy(self.nodes) + +class CartesianNode(): + + """ CartesianNode Class - The Cartesian Node representation + **Examples** + The instantiation + >>> node = G2DCartesian.CartesianNode(1, 0, + {"func3" : 3}, [self.prev1, self.prev2]) + + Very important thing for future developer is passing correct values for + data_set and previous_nodes. They are slightly different for input, + internal and output nodes in Cartesian Genetic Programming. + + Input node: + >>> node = G2DCartesian.CartesianNode(1, 1, {"b" : 1}, []) + No previous nodes and every function in data_set must have not more than + one argument (for 'args', in fact it is zero arguments). + Internal node: + >>> node = G2DCartesian.CartesianNode(1, 1, {"f1" : 3}, + [self.prev1, self.prev2]) + It should have previous_nodes (at least input nodes!) and there are no + constraints on functions dictionary and their inputs. + Output node: + >>> node = G2DCartesian.CartesianNode(1, 1, {}, + [self.prev1, self.prev2]) + No functions passed to output nodes, they just map net to the world. In + previous nodes they should have all existing nodes except those being + outputs. + + :param position_x: row position in the net + :param position_y: column position in the net + :param data_set: dictionary of functions which can be assigned to this node + and number of their args + :param previous_nodes: list of nodes which can be connected as inputs to + this node + """ + + paramMapping = {} + + def __init__(self, position_x, position_y, data_set = {}, + previous_nodes = []): + self.data = None + self.inputs = [] + self.params = {} + self.x = position_x + self.y = position_y + + try: + self.data = random.choice(data_set.keys()) + except IndexError: + self.data = "" + + try: + inputs_count = data_set[self.data]-1 + except KeyError: + inputs_count = 1 + + if (len(previous_nodes) < inputs_count): + raise ValueError("Bad data set and previous nodes values " + "combination. If specified data set with input args" + " then previous nodes can not be empty!") + + for i in range(0, inputs_count): + self.inputs.append(random.choice(previous_nodes)) + + for param in CartesianNode.paramMapping.keys(): + self.params[param] = eval(CartesianNode.paramMapping[param]) + + def getData(self): + return (self.data, len(self.inputs)) + + def __repr__(self): + ret = "\n\tCartesianNode [%s, %s] - " % (self.x, self.y) + ret += "Data: %s" % (self.data) + for i in self.inputs: + ret += " Input: [%s, %s]" % (i.x, i.y) + return ret diff --git a/pyevolve/Initializators.py b/pyevolve/Initializators.py index ff99c8b..b3ce811 100644 --- a/pyevolve/Initializators.py +++ b/pyevolve/Initializators.py @@ -16,6 +16,7 @@ from random import randint as rand_randint, uniform as rand_uniform, choice as rand_choice import GTree +import G2DCartesian import Util @@ -272,3 +273,77 @@ def GTreeGPInitializator(genome, **args): genome.setRoot(root) genome.processNodes() assert genome.getHeight() <= max_depth + +#################### +## Cartesian GP ## +#################### + +def G2DCartesianInitializatorNode(genome, **args): + """This initializator is for Cartesian Genetic Programming, it uses three + types of "slicers" from genome: inputs, internals and outputs. Every + input get single terminal from engine, internals get a set of possible + functions, their parameters mapping set and previous available nodes + outputs get just previous available nodes. From ga_engine is uses: + + *gp_function_set* + Dict of functions and their arguments counter founded automatically + after defining function prefix in ga_engine. + + *gp_terminals* + List of terminals passed to ga_engine. + + *gp_args_mapping* + Dict of parameters for node with value being a str generating value for + them via eval(), example: + {"param1" : "random.randint(0,10)"} + uses for parameter 'param1' random integer generator. + + .. versionadded:: + The *G2DCartesianInitializatorNode* function. + """ + + if not isinstance(genome, G2DCartesian.G2DCartesian): + raise TypeError("Specified genome unsuitable for this Initializator.") + + ga_engine = args["ga_engine"] + inputs = genome.inputs + outputs = genome.outputs + rows = genome.rows + cols = genome.cols + inputSlice = genome.inputSlice + internalSlice = genome.internalSlice + outputSlice = genome.outputSlice + terminals = ga_engine.getParam("gp_terminals") + functions_set = ga_engine.getParam("gp_function_set") + args_mapping = ga_engine.getParam("gp_args_mapping") + + if terminals is None: + raise AssertionError("Empty terminal set.") + if functions_set is None: + raise AssertionError("Empty function set.") + if args_mapping is None: + raise AssertionError("Empty argument mapping set.") + if not len(terminals) == inputs: + raise AssertionError("Terminal set must be equal with input length.") + + G2DCartesian.CartesianNode.paramMapping = args_mapping + + nodes = [] + for counter, terminal in enumerate(terminals): + nodes.append(G2DCartesian.CartesianNode(counter, -1, {terminal : 1})) + genome[inputSlice] = nodes + + previous_nodes = genome[0:inputs] + nodes = [] + for counter in xrange(0, rows * cols): + nodes.append(G2DCartesian.CartesianNode(counter / rows, counter % cols, + functions_set, previous_nodes)) + previous_nodes += genome[counter-rows:counter*((counter % cols) == + (cols + 1))] + genome[internalSlice] = nodes + + nodes = [] + for counter in xrange(0, outputs): + nodes.append(G2DCartesian.CartesianNode(counter, rows, {}, + previous_nodes)) + genome[outputSlice] = nodes diff --git a/tests/test_genomes.py b/tests/test_genomes.py new file mode 100644 index 0000000..6b0f81d --- /dev/null +++ b/tests/test_genomes.py @@ -0,0 +1,88 @@ +import unittest + +from mock import MagicMock +from pyevolve.G2DCartesian import G2DCartesian, CartesianNode + +class G2DCartesianGenomeTestCase(unittest.TestCase): + def setUp(self): + self.rows = 4 + self.cols = 5 + self.ins = 1 + self.outs = 3 + self.genome = G2DCartesian(self.rows, self.cols, self.ins, self.outs) + + def tearDown(self): + self.genome = None + + def test_genome_init(self): + self.assertTrue(self.genome.rows == self.rows) + self.assertTrue(self.genome.cols == self.cols) + self.assertTrue(self.genome.inputs == self.ins) + self.assertTrue(self.genome.outputs == self.outs) + self.assertTrue(self.genome.internalNodes == self.rows * self.cols) + self.assertTrue(self.genome.inputSlice == slice(0,self.ins)) + self.assertTrue(self.genome.internalSlice == slice(self.ins, self.ins + + (self.rows * self.cols))) + self.assertTrue(self.genome.outputSlice == slice(self.ins + + (self.rows * self.cols), + self.ins + + (self.rows * self.cols) + + self.outs)) + + def test_genome_clone(self): + genomeClone = self.genome.clone() + self.assertTrue(genomeClone == self.genome) + + def test_genome_nodes(self): + self.assertTrue(len(self.genome) == self.rows * self.cols + self.ins + + self.outs) + self.assertTrue(self.genome[0] == None) + self.assertTrue(self.genome[self.rows * self.cols + self.ins + + self.outs - 1] == None) + self.assertRaises(IndexError, self.genome.__getitem__, + self.rows*self.cols*self.cols) + + def test_genome_zero_param(self): + self.assertRaises(ValueError, G2DCartesian, 0, 1, 2, 10) + +class CartesianNodeTestCase(unittest.TestCase): + def setUp(self): + self.prev1 = CartesianNode(0, 1, {"a" : 1}, []) + self.prev2 = CartesianNode(0, 2, {"b" : 1}, []) + self.node = CartesianNode(1, 0, {"func3" : 3}, [self.prev1, self.prev2]) + + def tearDown(self): + self.node = None + CartesianNode.paramMapping.clear() + + def test_node_init(self): + self.assertFalse(self.node.data == None) + self.assertTrue(len(self.node.params) == 0) + + def test_node_init_input_like(self): + node = CartesianNode(1, 1, {"b" : 1}, []) + data = node.getData() + self.assertTrue(data[0] == "b") + self.assertTrue(data[1] == 0) + + def test_node_init_internal_like(self): + node = CartesianNode(1, 1, {"f1" : 3}, [self.prev1, self.prev2]) + data = node.getData() + self.assertTrue(data[0] == "f1") + self.assertTrue(data[1] == 2) + + def test_node_init_output_like(self): + node = CartesianNode(1, 1, {}, [self.prev1, self.prev2]) + data = node.getData() + self.assertTrue(data[0] == "") + self.assertTrue(data[1] == 1) + + def test_node_init_empty_previous_with_bad_function_set(self): + self.assertRaises(ValueError, CartesianNode, 0, 0, {"f2" : 3}) + + def test_node_param_mapping(self): + CartesianNode.paramMapping = {"p1" : "random.randint(0, 10)"} + node = CartesianNode(1, 1, {"f1" : 3}, [self.prev1, self.prev2]) + self.assertTrue(len(node.params) == len(CartesianNode.paramMapping)) + + \ No newline at end of file diff --git a/tests/test_initializators.py b/tests/test_initializators.py index f7dcef3..6ee6e4c 100644 --- a/tests/test_initializators.py +++ b/tests/test_initializators.py @@ -1,10 +1,13 @@ import unittest +from mock import MagicMock from pyevolve.G1DBinaryString import G1DBinaryString from pyevolve import Initializators from pyevolve.G1DList import G1DList from pyevolve.G2DList import G2DList from pyevolve.GTree import GTree +from pyevolve.G2DCartesian import G2DCartesian +import random class InitializatorsTestCase(unittest.TestCase): @@ -40,3 +43,168 @@ def test_tree_integer_initializator(self): Initializators.GTreeInitializatorInteger(genome) for gen in genome.getAllNodes(): self.assertTrue(type(gen.getData()) == int) + +class G2DCartesianInitializatorTestCase(unittest.TestCase): + @classmethod + def setUpClass(self): + self.engine = MagicMock(); + def getParams(value): + if value == "gp_terminals": + return ['a', 'b', 'c', 'd'] + elif value == "gp_function_set": + return {"gp1" : 2, "gp2" : 2, "gp3" : 3} + elif value == "gp_args_mapping": + return {"arg1" : "random.randint(0,10)", + "arg2" : "random.uniform(2.0,4.2)"} + + self.engine.getParam = MagicMock(side_effect=getParams) + + def setUp(self): + self.genome = G2DCartesian(2, 3, 4, 1) + + def tearDown(self): + self.genome = None + + def test_nodes_creation(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + for node in self.genome: + self.assertFalse(node == None) + + def test_input_nodes_inputs(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + for node in self.genome[self.genome.inputSlice]: + self.assertTrue(len(node.inputs) == 0) + + def test_internal_nodes_inputs(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + for node in self.genome[self.genome.internalSlice]: + self.assertTrue(len(node.inputs) in xrange(1,3)) + for input in node.inputs: + self.assertTrue(input.y < node.y) + + def test_output_nodes_inputs(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + for node in self.genome[self.genome.outputSlice]: + self.assertTrue(len(node.inputs) == 1) + for input in node.inputs: + self.assertTrue(input.y < node.y) + + def test_input_nodes_data_sets(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + for node in self.genome[self.genome.inputSlice]: + data = node.getData() + self.assertTrue(data[0] in self.engine.getParam("gp_terminals")) + + def test_internal_nodes_data_sets(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + for node in self.genome[self.genome.internalSlice]: + data = node.getData() + self.assertTrue(data[0] in self.engine.getParam("gp_function_set")) + + + def test_output_nodes_data_sets(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + for node in self.genome[self.genome.outputSlice]: + data = node.getData() + self.assertTrue(data[0] == "") + + def test_input_nodes_positions(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + for idx, node in enumerate(self.genome[self.genome.inputSlice]): + self.assertTrue(node.x == idx) + self.assertTrue(node.y == -1) + + def test_internal_nodes_positions(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + for idx, node in enumerate(self.genome[self.genome.internalSlice]): + self.assertTrue(node.x == idx / self.genome.rows) + self.assertTrue(node.y == idx % self.genome.cols) + + def test_output_nodes_positions(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + for idx, node in enumerate(self.genome[self.genome.outputSlice]): + self.assertTrue(node.x == idx) + self.assertTrue(node.y == self.genome.rows) + + def test_nodes_params(self): + Initializators.G2DCartesianInitializatorNode(self.genome, + ga_engine=self.engine) + mapping = self.engine.getParam("gp_args_mapping") + for node in self.genome: + for key in mapping.keys(): + self.assertTrue(key in node.params) + self.assertTrue(type(eval(mapping[key])) == + type(node.params[key])) + + def test_empty_functions(self): + eng = MagicMock() + def getParams(value): + if value == "gp_terminals": + return ['a', 'b', 'c', 'd'] + return None + + eng.getParam = MagicMock(side_effect=getParams) + self.assertRaises(AssertionError, + Initializators.G2DCartesianInitializatorNode, + self.genome, ga_engine=eng) + + def test_empty_terminals(self): + eng = MagicMock() + def getParams(value): + if value == "gp_function_set": + return {"gp1" : 2, "gp2" : 2, "gp3" : 3} + return None + + eng.getParam = MagicMock(side_effect=getParams) + self.assertRaises(AssertionError, + Initializators.G2DCartesianInitializatorNode, + self.genome, ga_engine=eng) + + def test_empty_mapping(self): + eng = MagicMock() + def getParams(value): + if value == "gp_terminals": + return ['a', 'b', 'c', 'd'] + elif value == "gp_function_set": + return {"gp1" : 2, "gp2" : 2, "gp3" : 3} + return None + + eng.getParam = MagicMock(side_effect=getParams) + self.assertRaises(AssertionError, + Initializators.G2DCartesianInitializatorNode, + self.genome, ga_engine=eng) + + def test_empty_engine(self): + self.assertRaises(KeyError, + Initializators.G2DCartesianInitializatorNode, + self.genome) + + def test_mismatch_input_with_terminals(self): + eng = MagicMock() + def getParams(value): + if value == "gp_terminals": + return ['a', 'b', 'c'] + if value == "gp_function_set": + return {"gp1" : 2, "gp2" : 2, "gp3" : 3} + + eng.getParam = MagicMock(side_effect=getParams) + self.assertRaises(AssertionError, + Initializators.G2DCartesianInitializatorNode, + self.genome, ga_engine=eng) + + def test_bad_genome(self): + genome = G1DList() + self.assertRaises(TypeError, + Initializators.G2DCartesianInitializatorNode, + genome) + From 16163d10270a932629619a6969c7fe6077f19d75 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Fri, 1 Jul 2016 23:53:25 +0200 Subject: [PATCH 02/20] New mutators and CGP genome update Added 4 mutators (input, parameters, function and order of nodes in active path) for cartesian genetic programming and new methods for CGP genome to handle new mutators. --- pyevolve/Consts.py | 2 + pyevolve/G2DCartesian.py | 62 +++++++++++++++++++--- pyevolve/Mutators.py | 111 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 7 deletions(-) diff --git a/pyevolve/Consts.py b/pyevolve/Consts.py index b942f7c..96ce4e9 100644 --- a/pyevolve/Consts.py +++ b/pyevolve/Consts.py @@ -494,6 +494,8 @@ # - G2DCartesian defaults CDefG2DCartesianInit = Initializators.G2DCartesianInitializatorNode +CDefG2DCartesianCrossover = None +CDefG2DCartesianMutator = Mutators.G2DCartesianMutatorNodeFunction # Gaussian Gradient CDefGaussianGradientMU = 1.0 diff --git a/pyevolve/G2DCartesian.py b/pyevolve/G2DCartesian.py index e60a0f7..fa636ef 100644 --- a/pyevolve/G2DCartesian.py +++ b/pyevolve/G2DCartesian.py @@ -61,7 +61,9 @@ class G2DCartesian(GenomeBase): "inputSlice", "internalSlice", "outputSlice", "nodes"] def __init__(self, rows, cols, inputs, outputs, cloning = False): - + """ The initializator of G2DCartesian representation, + rows, cols, inputs and outputs must be specified and none of them can + equal to 0""" if (rows * cols * inputs * outputs) == 0: raise ValueError("One of the genome parameter equals 0.") @@ -83,21 +85,26 @@ def __init__(self, rows, cols, inputs, outputs, cloning = False): self.crossover.set(Consts.CDefG2DCartesianCrossover) def __eq__(self, other): + """ Compares one chromosome with another """ cond1 = (self.nodes == other.nodes) cond2 = (self.rows == other.rows) cond3 = (self.cols == other.cols) return True if cond1 and cond2 and cond3 else False def __getitem__(self, key): + """ Return the specified node of net (including inputs and outputs)""" return self.nodes[key] def __iter__(self): + """ Iterator support to the nodes """ return iter(self.nodes) def __len__(self): + """ Return the number of all nodes in net """ return len(self.nodes) def __repr__(self): + """ Return a string representation of Genome """ ret = GenomeBase.__repr__(self) ret += "- G2DCartesian\n" ret += "\tList size:\t %s\n" % (len(self.nodes,)) @@ -105,17 +112,41 @@ def __repr__(self): return ret def __setitem__(self, key, value): + """ Set the specified node in net """ self.nodes[key] = value def clone(self): + """ Return a new instace copy of the genome + + :rtype: the G2DCartesian clone instance + + """ newcopy = G2DCartesian(self.rows, self.cols, self.inputs, self.outputs, True) self.copy(newcopy) return newcopy def copy(self, g): + """ Copy genome to 'g' + + Example: + >>> genome_origin.copy(genome_destination) + + :param g: the destination G2DCartesian instance + + """ GenomeBase.copy(self, g) g.nodes = copy.deepcopy(self.nodes) + + def getActiveNodes(self): + """ Return list of lists with active paths in net, the size of list + depends on the number of net outputs. It populates list in reverse + direction, from output to input """ + actives = [] + for i in xrange(0, self.outputs): + actives.append([]) + self.nodes[-i-1].getPreviousNodes(actives[i]) + return actives class CartesianNode(): @@ -156,7 +187,10 @@ class CartesianNode(): paramMapping = {} def __init__(self, position_x, position_y, data_set = {}, - previous_nodes = []): + previous_nodes = []): + """ The initializator of CartesianNode representation, + position_x and position_y must be specified, data_set and previous_nodes + depends on type of the node """ self.data = None self.inputs = [] self.params = {} @@ -183,13 +217,27 @@ def __init__(self, position_x, position_y, data_set = {}, for param in CartesianNode.paramMapping.keys(): self.params[param] = eval(CartesianNode.paramMapping[param]) - - def getData(self): - return (self.data, len(self.inputs)) - + def __repr__(self): + """ Return a string representation of Genome """ ret = "\n\tCartesianNode [%s, %s] - " % (self.x, self.y) ret += "Data: %s" % (self.data) for i in self.inputs: ret += " Input: [%s, %s]" % (i.x, i.y) - return ret + return ret + + def getData(self): + """ Return tuple with node value and number of its input args """ + return (self.data, len(self.inputs)) + + def getPreviousNodes(self, nodes): + """ Recursively returns previous, connected nodes in net of this node""" + if len(self.inputs) == 0: + return + elif self.data is not "": + nodes.append(self) + + for i in self.inputs: + i.getPreviousNodes(nodes) + + diff --git a/pyevolve/Mutators.py b/pyevolve/Mutators.py index 3c757df..afe74a7 100644 --- a/pyevolve/Mutators.py +++ b/pyevolve/Mutators.py @@ -12,6 +12,7 @@ from random import choice as rand_choice import Consts import GTree +from G2DCartesian import CartesianNode ############################# ## 1D Binary String ## @@ -1132,3 +1133,113 @@ def GTreeGPMutatorSubtree(genome, **args): genome.processNodes() return int(mutations) + +################### +## Cartesian GP ## +################### + + def G2DCartesianMutatorNodeInputs(genome, **args): + """ The mutator of G2DCartesian, Node inputs mutator + + This mutator will change inputs of node using available previous nodes. + + .. versionadded:: + The *G2DCartesianMutatorNodeInputs* function + """ + mutations = args["pmut"] * (genome.rows * genome.cols + genome.outputs + + genome.inputs) + if mutations < 1.0: + mutations = 1 + + for i in xrange(0, int(mutations)): + choosen = rand_choice(genome.nodes[genome.inputs:]) + previous_nodes = genome.nodes[:(genome.rows * choosen.y + genome.inputs)] + for idx, input in enumerate(node.inputs): + node.inputs[idx] = rand_choice(previous_nodes) + + return int(mutations) + + + def G2DCartesianMutatorNodeParams(genome, **args): + """ The mutator of G2DCartesian, Node params mutator + + This mutator will generate new values for parameters of node using parameters + mapping. + + .. versionadded:: + The *G2DCartesianMutatorNodeParams* function + """ + mutations = args["pmut"] * (genome.rows * genome.cols + genome.outputs + + genome.inputs) + if mutations < 1.0: + mutations = 1 + + for i in xrange(0, int(mutations)): + choosen = rand_choice(genome.nodes[genome.inputs:-genome.outputs]) + for key in choosen.params.keys(): + choosen.params[key] = eval(CartesianNode.paramMapping[key]) + + return int(mutations) + +def G2DCartesianMutatorNodeFunction(genome, **args): + """ The mutator of G2DCartesian, Node value mutator + + This mutator will change value of node (function) using available function + set. + + .. versionadded:: + The *G2DCartesianMutatorNodeFunction* function + """ + mutations = args["pmut"] * (genome.rows * genome.cols + genome.outputs + + genome.inputs) + ga_engine = args["ga_engine"] + if mutations < 1.0: + mutations = 1 + + function_set = ga_engine.getParam("gp_function_set") + previous_nodes = genome.nodes[:(genome.rows * choosen.y + genome.inputs)] + + for i in xrange(0, int(mutations)): + choosen = rand_choice(genome.nodes[genome.inputs:-genome.outputs]) + choosen.data = rand_choice(function_set.keys() + + if len(choosen.inputs) > function_set[choosen.data]-1: + del choosen.inputs[-1] + elif len(choosen.inputs) < function_set[choosen.data]-1: + choosen.inputs.append(random.choice(previous_nodes)) + + return int(mutations) + +def G2DCartesianMutatorNodesOrder(genome, **args): + """ The mutator of G2DCartesian, Nodes order in active path mutator + + This mutator will recreate order of nodes in active path, preserving values + of nodes. It can also manipulate inputs of nodes if current inputs state does + not satisfy new node value args. + + .. versionadded:: + The *G2DCartesianMutatorNodesOrder* function + """ + paths = genome.getActiveNodes() + mutations = 0 + for path in paths: + shuffled_functions = [] + for node in path: + shuffled_functions.append(node.getData()) + rand_shuffle(shuffled_functions) + + for idx, node in enumerate(path): + new_function = shuffled_functions[idx] + previous_nodes = genome.nodes[:(genome.rows * node.y + + genome.inputs)] + node.data = new_function[0] + inputs_diff = len(node.inputs) - (new_function[1]-1) + + if inputs_diff > 0: + del node.inputs[-inputs_diff:] + elif inputs_diff < 0: + for i in xrange(0, -inputs_diff) + node.inputs.append(random.choice(previous_nodes)) + mutations += len(path) + + return mutations From 311c988b33e584a5cc51c6dacc3d7c5ff39cc3b0 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Sat, 2 Jul 2016 23:59:30 +0200 Subject: [PATCH 03/20] CGP tests + crossover + whitespaces fix Added tests for new genome functions, whitespaces fixed and default crossover for CGP, --- pyevolve/Consts.py | 2 +- pyevolve/Crossovers.py | 7 +++++++ pyevolve/Mutators.py | 31 +++++++++++++++---------------- tests/test_genomes.py | 27 ++++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/pyevolve/Consts.py b/pyevolve/Consts.py index 96ce4e9..428470d 100644 --- a/pyevolve/Consts.py +++ b/pyevolve/Consts.py @@ -494,7 +494,7 @@ # - G2DCartesian defaults CDefG2DCartesianInit = Initializators.G2DCartesianInitializatorNode -CDefG2DCartesianCrossover = None +CDefG2DCartesianCrossover = Crossovers.G2DCartesianCrossoverNode CDefG2DCartesianMutator = Mutators.G2DCartesianMutatorNodeFunction # Gaussian Gradient diff --git a/pyevolve/Crossovers.py b/pyevolve/Crossovers.py index 7584a00..3c0f264 100644 --- a/pyevolve/Crossovers.py +++ b/pyevolve/Crossovers.py @@ -798,3 +798,10 @@ def GTreeGPCrossoverSinglePoint(genome, **args): assert brother.getHeight() <= max_depth return (sister, brother) + +############################################################################# +################# G2DCartesian Crossovers ################################# +############################################################################# + +def G2DCartesianCrossoverNode(genome, **args): + return (args["mom"].clone(), args["dad"].clone()) \ No newline at end of file diff --git a/pyevolve/Mutators.py b/pyevolve/Mutators.py index afe74a7..c8de39c 100644 --- a/pyevolve/Mutators.py +++ b/pyevolve/Mutators.py @@ -1138,7 +1138,7 @@ def GTreeGPMutatorSubtree(genome, **args): ## Cartesian GP ## ################### - def G2DCartesianMutatorNodeInputs(genome, **args): +def G2DCartesianMutatorNodeInputs(genome, **args): """ The mutator of G2DCartesian, Node inputs mutator This mutator will change inputs of node using available previous nodes. @@ -1160,15 +1160,15 @@ def G2DCartesianMutatorNodeInputs(genome, **args): return int(mutations) - def G2DCartesianMutatorNodeParams(genome, **args): - """ The mutator of G2DCartesian, Node params mutator +def G2DCartesianMutatorNodeParams(genome, **args): + """ The mutator of G2DCartesian, Node params mutator - This mutator will generate new values for parameters of node using parameters - mapping. + This mutator will generate new values for parameters of node using parameters + mapping. - .. versionadded:: + .. versionadded:: The *G2DCartesianMutatorNodeParams* function - """ + """ mutations = args["pmut"] * (genome.rows * genome.cols + genome.outputs + genome.inputs) if mutations < 1.0: @@ -1184,12 +1184,12 @@ def G2DCartesianMutatorNodeParams(genome, **args): def G2DCartesianMutatorNodeFunction(genome, **args): """ The mutator of G2DCartesian, Node value mutator - This mutator will change value of node (function) using available function - set. + This mutator will change value of node (function) using available function + set. - .. versionadded:: + .. versionadded:: The *G2DCartesianMutatorNodeFunction* function - """ + """ mutations = args["pmut"] * (genome.rows * genome.cols + genome.outputs + genome.inputs) ga_engine = args["ga_engine"] @@ -1201,9 +1201,9 @@ def G2DCartesianMutatorNodeFunction(genome, **args): for i in xrange(0, int(mutations)): choosen = rand_choice(genome.nodes[genome.inputs:-genome.outputs]) - choosen.data = rand_choice(function_set.keys() + choosen.data = rand_choice(function_set.keys()) - if len(choosen.inputs) > function_set[choosen.data]-1: + if len(choosen.inputs) > (function_set[choosen.data]-1): del choosen.inputs[-1] elif len(choosen.inputs) < function_set[choosen.data]-1: choosen.inputs.append(random.choice(previous_nodes)) @@ -1230,15 +1230,14 @@ def G2DCartesianMutatorNodesOrder(genome, **args): for idx, node in enumerate(path): new_function = shuffled_functions[idx] - previous_nodes = genome.nodes[:(genome.rows * node.y + - genome.inputs)] + previous_nodes = genome.nodes[:(genome.rows * node.y + genome.inputs)] node.data = new_function[0] inputs_diff = len(node.inputs) - (new_function[1]-1) if inputs_diff > 0: del node.inputs[-inputs_diff:] elif inputs_diff < 0: - for i in xrange(0, -inputs_diff) + for i in xrange(0, -inputs_diff): node.inputs.append(random.choice(previous_nodes)) mutations += len(path) diff --git a/tests/test_genomes.py b/tests/test_genomes.py index 6b0f81d..d6fe870 100644 --- a/tests/test_genomes.py +++ b/tests/test_genomes.py @@ -43,7 +43,14 @@ def test_genome_nodes(self): self.rows*self.cols*self.cols) def test_genome_zero_param(self): - self.assertRaises(ValueError, G2DCartesian, 0, 1, 2, 10) + self.assertRaises(ValueError, G2DCartesian, 0, 1, 2, 10) + + def test_genome_active_nodes(self): + for key, node in enumerate(self.genome): + self.genome[key] = MagicMock() + self.genome[key].getPreviousNodes = MagicMock(return_value = []) + paths = self.genome.getActiveNodes() + self.assertTrue(len(paths) == 3) class CartesianNodeTestCase(unittest.TestCase): def setUp(self): @@ -85,4 +92,22 @@ def test_node_param_mapping(self): node = CartesianNode(1, 1, {"f1" : 3}, [self.prev1, self.prev2]) self.assertTrue(len(node.params) == len(CartesianNode.paramMapping)) + def test_node_previous_for_input(self): + node = CartesianNode(1, 1, {"b" : 1}, []) + previous = [] + node.getPreviousNodes(previous) + self.assertTrue(len(previous) == 0) + + def test_node_previous_for_internal(self): + previous = [] + self.node.getPreviousNodes(previous) + self.assertTrue(len(previous) > 0) + + def test_node_previous_for_output(self): + prev = CartesianNode(1, 1, {"f" : 2}, [self.prev1, self.prev2]) + node = CartesianNode(1, 1, {}, [prev]) + previous = [] + node.getPreviousNodes(previous) + self.assertTrue(len(previous) > 0) + \ No newline at end of file From 20337732ba8f97d5bbdbc786be1168051b23a407 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Mon, 4 Jul 2016 11:31:24 +0200 Subject: [PATCH 04/20] Update README --- README | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README b/README index 5590741..70bda44 100644 --- a/README +++ b/README @@ -1,3 +1,5 @@ -This is the new official Pyevolve repository. +This is the fork of pyevolve framework. -The documentation (html rendered) is still hosted at sourceforge.net at http://pyevolve.sourceforge.net/0_6rc1/ +The main puprose is to add a Cartesian Genetic Programming (CGP) genome to the evolution schema. + +Also some fixes and reformation added. From 84f003b799b25b9456c2e46eeb62f4e43d722360 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Tue, 5 Jul 2016 15:42:09 +0200 Subject: [PATCH 05/20] Tests and fixes Added mutators and crossover tests, few bugs in mutators and cartesian genome fixed. --- pyevolve/G2DCartesian.py | 2 +- pyevolve/Mutators.py | 51 ++++++------ tests/test_crossovers.py | 16 ++++ tests/test_mutators.py | 164 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 208 insertions(+), 25 deletions(-) diff --git a/pyevolve/G2DCartesian.py b/pyevolve/G2DCartesian.py index fa636ef..0db016b 100644 --- a/pyevolve/G2DCartesian.py +++ b/pyevolve/G2DCartesian.py @@ -207,7 +207,7 @@ def __init__(self, position_x, position_y, data_set = {}, except KeyError: inputs_count = 1 - if (len(previous_nodes) < inputs_count): + if (len(previous_nodes) == 0 and inputs_count > 0): raise ValueError("Bad data set and previous nodes values " "combination. If specified data set with input args" " then previous nodes can not be empty!") diff --git a/pyevolve/Mutators.py b/pyevolve/Mutators.py index c8de39c..9c343d7 100644 --- a/pyevolve/Mutators.py +++ b/pyevolve/Mutators.py @@ -9,7 +9,7 @@ import Util from random import randint as rand_randint, gauss as rand_gauss, uniform as rand_uniform -from random import choice as rand_choice +from random import choice as rand_choice, shuffle as rand_shuffle import Consts import GTree from G2DCartesian import CartesianNode @@ -1153,9 +1153,10 @@ def G2DCartesianMutatorNodeInputs(genome, **args): for i in xrange(0, int(mutations)): choosen = rand_choice(genome.nodes[genome.inputs:]) - previous_nodes = genome.nodes[:(genome.rows * choosen.y + genome.inputs)] - for idx, input in enumerate(node.inputs): - node.inputs[idx] = rand_choice(previous_nodes) + previous_nodes = genome.nodes[:(genome.rows * choosen.y + + genome.inputs)] + for idx, input in enumerate(choosen.inputs): + choosen.inputs[idx] = rand_choice(previous_nodes) return int(mutations) @@ -1163,8 +1164,8 @@ def G2DCartesianMutatorNodeInputs(genome, **args): def G2DCartesianMutatorNodeParams(genome, **args): """ The mutator of G2DCartesian, Node params mutator - This mutator will generate new values for parameters of node using parameters - mapping. + This mutator will generate new values for parameters of node using + parameters mapping. .. versionadded:: The *G2DCartesianMutatorNodeParams* function @@ -1196,33 +1197,36 @@ def G2DCartesianMutatorNodeFunction(genome, **args): if mutations < 1.0: mutations = 1 - function_set = ga_engine.getParam("gp_function_set") - previous_nodes = genome.nodes[:(genome.rows * choosen.y + genome.inputs)] + function_set = ga_engine.getParam("gp_function_set") for i in xrange(0, int(mutations)): choosen = rand_choice(genome.nodes[genome.inputs:-genome.outputs]) choosen.data = rand_choice(function_set.keys()) - - if len(choosen.inputs) > (function_set[choosen.data]-1): - del choosen.inputs[-1] - elif len(choosen.inputs) < function_set[choosen.data]-1: - choosen.inputs.append(random.choice(previous_nodes)) + previous_nodes = genome.nodes[:(genome.rows * choosen.y + + genome.inputs)] + + inputs_diff = len(choosen.inputs) - (function_set[choosen.data]-1) + if inputs_diff > 0: + del choosen.inputs[-inputs_diff:] + elif inputs_diff < 0: + for i in xrange(0, -inputs_diff): + choosen.inputs.append(rand_choice(previous_nodes)) return int(mutations) def G2DCartesianMutatorNodesOrder(genome, **args): """ The mutator of G2DCartesian, Nodes order in active path mutator - This mutator will recreate order of nodes in active path, preserving values - of nodes. It can also manipulate inputs of nodes if current inputs state does - not satisfy new node value args. + This mutator will recreate order of nodes in active path, preserving values + of nodes. It can also manipulate inputs of nodes if current inputs state does + not satisfy new node value args. - .. versionadded:: + .. versionadded:: The *G2DCartesianMutatorNodesOrder* function - """ + """ paths = genome.getActiveNodes() mutations = 0 - for path in paths: + for path in paths: shuffled_functions = [] for node in path: shuffled_functions.append(node.getData()) @@ -1230,15 +1234,16 @@ def G2DCartesianMutatorNodesOrder(genome, **args): for idx, node in enumerate(path): new_function = shuffled_functions[idx] - previous_nodes = genome.nodes[:(genome.rows * node.y + genome.inputs)] - node.data = new_function[0] - inputs_diff = len(node.inputs) - (new_function[1]-1) + previous_nodes = genome.nodes[:(genome.rows * node.y + + genome.inputs)] + node.data = new_function[0] + inputs_diff = len(node.inputs) - (new_function[1]) if inputs_diff > 0: del node.inputs[-inputs_diff:] elif inputs_diff < 0: for i in xrange(0, -inputs_diff): - node.inputs.append(random.choice(previous_nodes)) + node.inputs.append(rand_choice(previous_nodes)) mutations += len(path) return mutations diff --git a/tests/test_crossovers.py b/tests/test_crossovers.py index 1f87a50..ca92258 100644 --- a/tests/test_crossovers.py +++ b/tests/test_crossovers.py @@ -10,6 +10,7 @@ from pyevolve.G2DBinaryString import G2DBinaryString from pyevolve.G2DList import G2DList from pyevolve.GTree import GTree, GTreeNode +from pyevolve.G2DCartesian import G2DCartesian class CrossoverTestCase(unittest.TestCase): @@ -390,3 +391,18 @@ def test_strict_single_point_crossover(self, rand_mock): assertion_name='assetTreesEqual', crossover_extra_kwargs={'count': 2} ) + +class G2DCartesianCrossoverTestCase(CrossoverTestCase): + def setUp(self): + self.mom = G2DCartesian(2, 2, 1, 1) + self.dad = G2DCartesian(2, 2, 1, 1) + + def test_crossover_node(self): + self.assertCrossoverResultsEqual( + Crossovers.G2DCartesianCrossoverNode, + self.mom, + self.dad, + None, + '' + ) + diff --git a/tests/test_mutators.py b/tests/test_mutators.py index 65c4d5d..9d89309 100644 --- a/tests/test_mutators.py +++ b/tests/test_mutators.py @@ -1,10 +1,12 @@ import unittest -from mock import patch +from mock import patch, Mock from pyevolve.G1DBinaryString import G1DBinaryString from pyevolve import Mutators, Consts from pyevolve.G1DList import G1DList +from pyevolve.G2DCartesian import G2DCartesian, CartesianNode +from random import randint class G1DBinaryStringMutatorsTestCase(unittest.TestCase): @@ -177,3 +179,163 @@ def test_binary_mutator_large_pmut(self, rand_mock): expected_result = [1, 2, 3] Mutators.G1DListMutatorIntegerBinary(self.genome, pmut=0.5) self.assertEqual(self.genome.genomeList, expected_result) + +class G2DCartesianMutatorsTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + CartesianNode.paramMapping.clear() + + def setUp(self): + self.function_set = {'f1' : 2, 'f3' : 3, 'f4' : 5} + self.genome = G2DCartesian(2, 2, 1, 1) + + self.genome[0] = CartesianNode(0, -1, {'a' : 1}, []) + prevs = [self.genome[0]] + self.genome[1] = CartesianNode(0, 0, {'f1' : 2}, prevs) + self.genome[2] = CartesianNode(1, 0, {'f2' : 2}, prevs) + prevs.append(self.genome[1]) + prevs.append(self.genome[2]) + self.genome[3] = CartesianNode(0, 1, {'f3' : 3}, prevs) + self.genome[4] = CartesianNode(1, 1, {'f4' : 3}, prevs) + prevs.append(self.genome[3]) + prevs.append(self.genome[4]) + self.genome[5] = CartesianNode(0, 2, {}, prevs) + + def side_param(arg): + if arg == "gp_function_set": + return self.function_set + + self.ga_engine = Mock() + self.ga_engine.getParam = side_param + + @patch('pyevolve.Mutators.rand_choice') + def test_cartesian_mutator_inputs_small_pmut(self, rand_mock): + expected_result = [] + values = [3, 0, 2] + def choice_effect(arg): + return self.genome[values.pop(0)] + + rand_mock.side_effect = choice_effect + + for i in values[1:]: + expected_result.append(self.genome[i]) + Mutators.G2DCartesianMutatorNodeInputs(self.genome, pmut=0.1) + self.assertEqual(self.genome[3].inputs, expected_result) + + @patch('pyevolve.Mutators.rand_choice') + def test_cartesian_mutator_inputs_large_pmut(self, rand_mock): + expected_result = {} + values = [1, 0, 2, 0 , 3, 1, 0, 4, 1, 2] + def choice_effect(arg): + return self.genome[values.pop(0)] + + rand_mock.side_effect = choice_effect + + idx = 0 + while idx < len(values): + expected_result[values[idx]] = [] + node_idx = idx + for input in self.genome[values[idx]].inputs: + idx = idx+1 + expected_result[values[node_idx]].append( + self.genome[values[idx]]) + idx = idx+1 + + Mutators.G2DCartesianMutatorNodeInputs(self.genome, pmut=0.7) + for key in expected_result.keys(): + self.assertEqual(self.genome[key].inputs, expected_result[key]) + + @patch('pyevolve.Mutators.rand_choice') + def test_cartesian_mutator_function_small_pmut(self, rand_mock): + values = [4, "f3"] + def choice_effect(arg): + what = values.pop(0) + if isinstance(what, int): + return self.genome[what] + else: + return what + + rand_mock.side_effect = choice_effect + Mutators.G2DCartesianMutatorNodeFunction(self.genome, pmut=0.1, + ga_engine = self.ga_engine) + self.assertEqual(self.genome[4].data, "f3") + self.assertEqual(len(self.genome[4].inputs), self.function_set["f3"]-1) + + @patch('pyevolve.Mutators.rand_choice') + def test_cartesian_mutator_function_large_pmut(self, rand_mock): + values = [1, "f3", 0, 2, "f4", 0, 0, 0, 3, "f1", 4, "f4", 0, 0] + def choice_effect(arg): + what = values.pop(0) + if isinstance(what, int): + return self.genome[what] + else: + return what + + rand_mock.side_effect = choice_effect + Mutators.G2DCartesianMutatorNodeFunction(self.genome, pmut=0.7, + ga_engine = self.ga_engine) + values = [1, "f3", 0, 2, "f4", 0, 0, 0, 3, "f1", 4, "f4", 0, 0] + idx = 0 + while idx >= 0: + values = values[idx:] + node_idx = values[0] + func = values[1] + self.assertEqual(self.genome[node_idx].data, func) + self.assertEqual(len(self.genome[node_idx].inputs), + self.function_set[func]-1) + values = values[2:] + idx = (next((key for key, val in + enumerate(values) if + isinstance(val, str)), 0) - 1) + + @patch('pyevolve.Mutators.rand_choice') + def test_cartesian_mutator_params_small_pmut(self, rand_mock): + CartesianNode.paramMapping = {"p1" : "rand_randint(0,10)", + "p2" : "rand_randint(0,10)"} + value = 3 + for param in CartesianNode.paramMapping.keys(): + self.genome[value].params[param] = -1 + rand_mock.return_value = self.genome[value] + Mutators.G2DCartesianMutatorNodeParams(self.genome, pmut=0.1) + for param in self.genome[value].params.values(): + self.assertTrue(param in range(0,11)) + CartesianNode.paramMapping.clear() + + @patch('pyevolve.Mutators.rand_choice') + def test_cartesian_mutator_params_large_pmut(self, rand_mock): + CartesianNode.paramMapping = {"p1" : "rand_randint(0,10)", + "p2" : "rand_randint(0,10)"} + values = [1, 2, 3, 4] + for v in values: + for param in CartesianNode.paramMapping.keys(): + self.genome[v].params[param] = -1 + def choice_effect(arg): + return self.genome[values.pop(0)] + rand_mock.side_effect = choice_effect + Mutators.G2DCartesianMutatorNodeParams(self.genome, pmut=0.7) + values = [1, 2, 3, 4] + for v in values: + for param in self.genome[v].params.values(): + self.assertTrue(param in range(0,11)) + CartesianNode.paramMapping.clear() + + @patch('pyevolve.Mutators.rand_shuffle') + def test_cartesian_mutator_order(self, rand_mock): + self.genome[5].inputs = [self.genome[4]] + self.genome[4].inputs = [self.genome[1]] + self.genome[1].inputs = [self.genome[0]] + + expected_order = [("f4", 5), ("f3", 1)] + def shuffle_effect(arg): + arg[:] = list(expected_order) + return + + rand_mock.side_effect = shuffle_effect + Mutators.G2DCartesianMutatorNodesOrder(self.genome, pmut=0.2) + paths = self.genome.getActiveNodes() + shuffled_functions = [] + for node in paths[0]: + shuffled_functions.append(node.getData()) + + for func in expected_order: + self.assertTrue(func in shuffled_functions) \ No newline at end of file From 1918b9260f16a697aa276a4526398603c4c68833 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Fri, 8 Jul 2016 13:01:17 +0200 Subject: [PATCH 06/20] Extended Cartesian Genome functionality Added new methods for CGP genome (for getting expression, compiled code of expression and writing expression to pydot graph), reverted orientation of the net (now is more intuitive), few bugs fixed and new tests added. --- pyevolve/G2DCartesian.py | 72 +++++++++++++++++++++++++++++--- pyevolve/Initializators.py | 15 +++---- pyevolve/Mutators.py | 12 +++--- tests/test_genomes.py | 79 +++++++++++++++++++++++++++++++++--- tests/test_initializators.py | 20 ++++----- 5 files changed, 164 insertions(+), 34 deletions(-) diff --git a/pyevolve/G2DCartesian.py b/pyevolve/G2DCartesian.py index 0db016b..cbebab8 100644 --- a/pyevolve/G2DCartesian.py +++ b/pyevolve/G2DCartesian.py @@ -22,7 +22,8 @@ from GenomeBase import GenomeBase import Consts -import random +from random import randint as rand_randint, choice as rand_choice +from random import uniform as rand_uniform, gauss as rand_gauss import pydot import copy @@ -67,7 +68,7 @@ def __init__(self, rows, cols, inputs, outputs, cloning = False): if (rows * cols * inputs * outputs) == 0: raise ValueError("One of the genome parameter equals 0.") - super(G2DCartesian, self).__init__() + super(G2DCartesian, self).__init__() self.rows = rows self.cols = cols self.inputs = inputs @@ -147,6 +148,43 @@ def getActiveNodes(self): actives.append([]) self.nodes[-i-1].getPreviousNodes(actives[i]) return actives + + def getCompiledCode(self): + """ Returns list of expressions from the genome, size of list depends on + outputs count. Expressions are already a python compile object """ + expr = [None] * self.outputs + for i in xrange(0, self.outputs): + expr[i] = self.nodes[-i-1].getExpression("") + compiled = [] + for e in expr: + compiled.append(compile(e, "", "eval")) + return compiled + + def writeDotGraph(self, graph): + """ Populates graph for pydot from active expression of genome """ + node_counter = 0 + for out in xrange(0, self.outputs): + node_stack = [] + node_dict = {} + node_stack.append(self.nodes[-1-out]) + while node_stack: + current_node = node_stack.pop() + if current_node.data != "": + node_label = current_node.data + for param in current_node.params.keys(): + node_label += " " + str(current_node.params[param]) + graph.add_node(pydot.Node(str(node_counter), + label = node_label)) + if current_node in node_dict: + graph.add_edge(pydot.Edge(node_dict[current_node], + str(node_counter))) + + for input in current_node.inputs: + node_stack.append(input) + if current_node.data != "": + node_dict[input] = str(node_counter) + if current_node.data != "": + node_counter += 1 class CartesianNode(): @@ -198,7 +236,7 @@ def __init__(self, position_x, position_y, data_set = {}, self.y = position_y try: - self.data = random.choice(data_set.keys()) + self.data = rand_choice(data_set.keys()) except IndexError: self.data = "" @@ -213,7 +251,7 @@ def __init__(self, position_x, position_y, data_set = {}, " then previous nodes can not be empty!") for i in range(0, inputs_count): - self.inputs.append(random.choice(previous_nodes)) + self.inputs.append(rand_choice(previous_nodes)) for param in CartesianNode.paramMapping.keys(): self.params[param] = eval(CartesianNode.paramMapping[param]) @@ -224,12 +262,36 @@ def __repr__(self): ret += "Data: %s" % (self.data) for i in self.inputs: ret += " Input: [%s, %s]" % (i.x, i.y) - return ret + return ret def getData(self): """ Return tuple with node value and number of its input args """ return (self.data, len(self.inputs)) + def getExpression(self, expr): + """ Recursively iterates through input nodes of current node and return + merged expression (function in the node and params) in string format """ + if self.data is not "": + expr += self.data + if self.inputs: + expr += "( " + input_counter = 0 + for idx, input in enumerate(self.inputs): + expr += input.getExpression("") + if idx < len(self.inputs)-1: + expr += ", " + + expr += ", {" + for idx, param in enumerate(self.params.keys()): + expr += "\"" + param + "\"" + " : " + expr += str(self.params[param]) + if idx < len(self.params)-1: + expr += ", " + expr += "} )" + else: + expr += self.inputs[0].getExpression("") + return expr + def getPreviousNodes(self, nodes): """ Recursively returns previous, connected nodes in net of this node""" if len(self.inputs) == 0: diff --git a/pyevolve/Initializators.py b/pyevolve/Initializators.py index b3ce811..f20a00a 100644 --- a/pyevolve/Initializators.py +++ b/pyevolve/Initializators.py @@ -14,7 +14,7 @@ """ -from random import randint as rand_randint, uniform as rand_uniform, choice as rand_choice +from random import randint as rand_randint, uniform as rand_uniform, choice as rand_choice, gauss as rand_gauss import GTree import G2DCartesian import Util @@ -330,20 +330,21 @@ def G2DCartesianInitializatorNode(genome, **args): nodes = [] for counter, terminal in enumerate(terminals): - nodes.append(G2DCartesian.CartesianNode(counter, -1, {terminal : 1})) + nodes.append(G2DCartesian.CartesianNode(-1, counter, {terminal : 1})) genome[inputSlice] = nodes previous_nodes = genome[0:inputs] nodes = [] for counter in xrange(0, rows * cols): - nodes.append(G2DCartesian.CartesianNode(counter / rows, counter % cols, - functions_set, previous_nodes)) - previous_nodes += genome[counter-rows:counter*((counter % cols) == - (cols + 1))] + nodes.append(G2DCartesian.CartesianNode(counter / cols, counter % cols, + functions_set, previous_nodes)) + start = (counter / cols) * cols + end = (start+cols)*((counter % cols) == (cols - 1)) + previous_nodes += nodes[start:end] genome[internalSlice] = nodes nodes = [] for counter in xrange(0, outputs): - nodes.append(G2DCartesian.CartesianNode(counter, rows, {}, + nodes.append(G2DCartesian.CartesianNode(rows, counter, {}, previous_nodes)) genome[outputSlice] = nodes diff --git a/pyevolve/Mutators.py b/pyevolve/Mutators.py index 9c343d7..95a173e 100644 --- a/pyevolve/Mutators.py +++ b/pyevolve/Mutators.py @@ -9,7 +9,7 @@ import Util from random import randint as rand_randint, gauss as rand_gauss, uniform as rand_uniform -from random import choice as rand_choice, shuffle as rand_shuffle +from random import choice as rand_choice, shuffle as rand_shuffle, gauss as rand_gauss import Consts import GTree from G2DCartesian import CartesianNode @@ -1150,10 +1150,10 @@ def G2DCartesianMutatorNodeInputs(genome, **args): genome.inputs) if mutations < 1.0: mutations = 1 - + for i in xrange(0, int(mutations)): choosen = rand_choice(genome.nodes[genome.inputs:]) - previous_nodes = genome.nodes[:(genome.rows * choosen.y + + previous_nodes = genome.nodes[:(genome.cols * choosen.x + genome.inputs)] for idx, input in enumerate(choosen.inputs): choosen.inputs[idx] = rand_choice(previous_nodes) @@ -1196,13 +1196,13 @@ def G2DCartesianMutatorNodeFunction(genome, **args): ga_engine = args["ga_engine"] if mutations < 1.0: mutations = 1 - + function_set = ga_engine.getParam("gp_function_set") for i in xrange(0, int(mutations)): choosen = rand_choice(genome.nodes[genome.inputs:-genome.outputs]) choosen.data = rand_choice(function_set.keys()) - previous_nodes = genome.nodes[:(genome.rows * choosen.y + + previous_nodes = genome.nodes[:(genome.cols * choosen.x + genome.inputs)] inputs_diff = len(choosen.inputs) - (function_set[choosen.data]-1) @@ -1234,7 +1234,7 @@ def G2DCartesianMutatorNodesOrder(genome, **args): for idx, node in enumerate(path): new_function = shuffled_functions[idx] - previous_nodes = genome.nodes[:(genome.rows * node.y + + previous_nodes = genome.nodes[:(genome.cols * node.x + genome.inputs)] node.data = new_function[0] inputs_diff = len(node.inputs) - (new_function[1]) diff --git a/tests/test_genomes.py b/tests/test_genomes.py index d6fe870..49709e0 100644 --- a/tests/test_genomes.py +++ b/tests/test_genomes.py @@ -1,7 +1,9 @@ import unittest -from mock import MagicMock +from mock import patch, MagicMock from pyevolve.G2DCartesian import G2DCartesian, CartesianNode +import re +from random import choice, randint class G2DCartesianGenomeTestCase(unittest.TestCase): def setUp(self): @@ -47,16 +49,73 @@ def test_genome_zero_param(self): def test_genome_active_nodes(self): for key, node in enumerate(self.genome): - self.genome[key] = MagicMock() - self.genome[key].getPreviousNodes = MagicMock(return_value = []) + mock = MagicMock() + inputs = [] + if key > 0: + for i in xrange(0, randint(1, 3)): + inputs.append(choice(self.genome[0:key])) + + mock.inputs = inputs + self.genome[key] = mock + self.genome[key].getPreviousNodes = MagicMock(return_value = inputs) paths = self.genome.getActiveNodes() self.assertTrue(len(paths) == 3) + + def test_genome_compiled_code(self): + from math import sin + expected_exprs = [] + values = [10, 50, 90] + def get_effect(arg): + expected_exprs.append("sin(" + str(values.pop(0)) + ")") + return expected_exprs[-1] + + for key, node in enumerate(self.genome): + mock = MagicMock() + mock.getExpression = MagicMock(side_effect = get_effect) + self.genome[key] = mock + + compiled = self.genome.getCompiledCode() + self.assertTrue(len(compiled) == self.outs) + for comp, expr in zip(compiled, expected_exprs): + self.assertEqual(eval(comp), eval(expr)) + + def test_genome_to_graph(self): + try: + import pydot + except ImportError: + return + + for key, node in enumerate(self.genome): + mock = MagicMock() + mock.params = {"p1" : 1} + if key >= self.rows * self.cols + self.ins: + mock.data = "" + else: + mock.data = str(key) + inputs = [] + if key > 0: + for i in xrange(0, randint(1, 3)): + inputs.append(choice(self.genome[0:key])) + + mock.inputs = inputs + self.genome[key] = mock + + graph = pydot.Dot(graph_type='graph') + self.genome.writeDotGraph(graph) + self.assertTrue(len(graph.get_node_list()) >= self.genome.outputs) class CartesianNodeTestCase(unittest.TestCase): - def setUp(self): + @patch('pyevolve.G2DCartesian.rand_randint') + @patch('pyevolve.G2DCartesian.rand_uniform') + def setUp(self, rand_uni, rand_int): + rand_uni.return_value = 0.1313 + rand_int.return_value = 8 + CartesianNode.paramMapping = {"p1" : "rand_randint(0, 10)", + "p2" : "rand_uniform(0,1)"} self.prev1 = CartesianNode(0, 1, {"a" : 1}, []) self.prev2 = CartesianNode(0, 2, {"b" : 1}, []) self.node = CartesianNode(1, 0, {"func3" : 3}, [self.prev1, self.prev2]) + self.outnode = CartesianNode(2, 0, {}, [self.node]) def tearDown(self): self.node = None @@ -64,7 +123,8 @@ def tearDown(self): def test_node_init(self): self.assertFalse(self.node.data == None) - self.assertTrue(len(self.node.params) == 0) + self.assertTrue(len(self.node.params) == + len(CartesianNode.paramMapping)) def test_node_init_input_like(self): node = CartesianNode(1, 1, {"b" : 1}, []) @@ -88,7 +148,7 @@ def test_node_init_empty_previous_with_bad_function_set(self): self.assertRaises(ValueError, CartesianNode, 0, 0, {"f2" : 3}) def test_node_param_mapping(self): - CartesianNode.paramMapping = {"p1" : "random.randint(0, 10)"} + CartesianNode.paramMapping = {"p1" : "rand_randint(0, 10)"} node = CartesianNode(1, 1, {"f1" : 3}, [self.prev1, self.prev2]) self.assertTrue(len(node.params) == len(CartesianNode.paramMapping)) @@ -109,5 +169,12 @@ def test_node_previous_for_output(self): previous = [] node.getPreviousNodes(previous) self.assertTrue(len(previous) > 0) + + def test_get_expression(self): + expected_expr = "func3( " + expr = self.outnode.getExpression("") + match = re.search('func3\( [a-z], [a-z], \{"[a-z0-9]*" : (\d+\.)?\d+, ' + '"[a-z0-9]*" : (\d+\.)?\d+\} \)', expr) + self.assertEqual(match.group(0), expr) \ No newline at end of file diff --git a/tests/test_initializators.py b/tests/test_initializators.py index 6ee6e4c..be11642 100644 --- a/tests/test_initializators.py +++ b/tests/test_initializators.py @@ -7,7 +7,7 @@ from pyevolve.G2DList import G2DList from pyevolve.GTree import GTree from pyevolve.G2DCartesian import G2DCartesian -import random +from random import randint as rand_randint, uniform as rand_uniform class InitializatorsTestCase(unittest.TestCase): @@ -54,8 +54,8 @@ def getParams(value): elif value == "gp_function_set": return {"gp1" : 2, "gp2" : 2, "gp3" : 3} elif value == "gp_args_mapping": - return {"arg1" : "random.randint(0,10)", - "arg2" : "random.uniform(2.0,4.2)"} + return {"arg1" : "rand_randint(0,10)", + "arg2" : "rand_uniform(2.0,4.2)"} self.engine.getParam = MagicMock(side_effect=getParams) @@ -83,7 +83,7 @@ def test_internal_nodes_inputs(self): for node in self.genome[self.genome.internalSlice]: self.assertTrue(len(node.inputs) in xrange(1,3)) for input in node.inputs: - self.assertTrue(input.y < node.y) + self.assertTrue(input.x < node.x) def test_output_nodes_inputs(self): Initializators.G2DCartesianInitializatorNode(self.genome, @@ -91,7 +91,7 @@ def test_output_nodes_inputs(self): for node in self.genome[self.genome.outputSlice]: self.assertTrue(len(node.inputs) == 1) for input in node.inputs: - self.assertTrue(input.y < node.y) + self.assertTrue(input.x < node.x) def test_input_nodes_data_sets(self): Initializators.G2DCartesianInitializatorNode(self.genome, @@ -119,22 +119,22 @@ def test_input_nodes_positions(self): Initializators.G2DCartesianInitializatorNode(self.genome, ga_engine=self.engine) for idx, node in enumerate(self.genome[self.genome.inputSlice]): - self.assertTrue(node.x == idx) - self.assertTrue(node.y == -1) + self.assertTrue(node.x == -1) + self.assertTrue(node.y == idx) def test_internal_nodes_positions(self): Initializators.G2DCartesianInitializatorNode(self.genome, ga_engine=self.engine) for idx, node in enumerate(self.genome[self.genome.internalSlice]): - self.assertTrue(node.x == idx / self.genome.rows) + self.assertTrue(node.x == idx / self.genome.cols) self.assertTrue(node.y == idx % self.genome.cols) def test_output_nodes_positions(self): Initializators.G2DCartesianInitializatorNode(self.genome, ga_engine=self.engine) for idx, node in enumerate(self.genome[self.genome.outputSlice]): - self.assertTrue(node.x == idx) - self.assertTrue(node.y == self.genome.rows) + self.assertTrue(node.x == self.genome.rows) + self.assertTrue(node.y == idx) def test_nodes_params(self): Initializators.G2DCartesianInitializatorNode(self.genome, From 40f913162e591ca8568257b0f9ff8d5875c3863c Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Sat, 9 Jul 2016 11:43:44 +0200 Subject: [PATCH 07/20] Overloaded evaluate and mutate in CGP genome. Performance issue modification + tests. Now evaluating CGP genome only if mutations changed expression structure, the bigger the net is the more profit we get from this change. It is also noticeable for slow fitness function. --- pyevolve/G2DCartesian.py | 44 ++++++++++++++++++++++++++---- pyevolve/Mutators.py | 29 ++++++++++++-------- tests/test_genomes.py | 59 ++++++++++++++++++++++++++++++++++++++++ tests/test_mutators.py | 37 +++++++++++++++++-------- 4 files changed, 141 insertions(+), 28 deletions(-) diff --git a/pyevolve/G2DCartesian.py b/pyevolve/G2DCartesian.py index cbebab8..1f7e60f 100644 --- a/pyevolve/G2DCartesian.py +++ b/pyevolve/G2DCartesian.py @@ -11,10 +11,13 @@ :func:`Initializators.G2DCartesianInitializatorNode` The Node Initializator for G2DCartesian *Mutator* - :func:`None` - The Mutator for G2DCartesian + :func:`Mutators.G2DCartesianMutatorNodeInputs` + :func:`Mutators.G2DCartesianMutatorNodeParams` + :func:`Mutators.G2DCartesianMutatorNodeFunction` + :func:`Mutators.G2DCartesianMutatorNodesOrder` + The Mutators for G2DCartesian *Crossover* - :func:`None` + :func:`Crossovers.G2DCartesianCrossoverNode` The Crossover for G2DCartesian Class ------------------------------------------------------------- @@ -59,7 +62,8 @@ class G2DCartesian(GenomeBase): """ __slots__ = ["inputs", "outputs", "cols", "rows", "internalNodes", - "inputSlice", "internalSlice", "outputSlice", "nodes"] + "inputSlice", "internalSlice", "outputSlice", "nodes", + "reevaluate", "expressionNodes"] def __init__(self, rows, cols, inputs, outputs, cloning = False): """ The initializator of G2DCartesian representation, @@ -79,6 +83,8 @@ def __init__(self, rows, cols, inputs, outputs, cloning = False): self.internalSlice = slice(self.inputs, self.inputs+self.internalNodes) self.outputSlice = slice(self.inputs+self.internalNodes, self.inputs+self.internalNodes+self.outputs) + self.expressionNodes = {} + self.reevaluate = True if not cloning: self.initializator.set(Consts.CDefG2DCartesianInit) @@ -139,6 +145,18 @@ def copy(self, g): GenomeBase.copy(self, g) g.nodes = copy.deepcopy(self.nodes) + def evaluate(self, **args): + """ Overloaded method of GenomeBase. It is performance improvement, + genome score is evaluated only when mutations had influence on nodes + active in the expression path """ + if self.reevaluate: + super(G2DCartesian, self).evaluate(**args) + self.expressionNodes.clear() + for path in self.getActiveNodes(): + print path + for node in path: + self.expressionNodes[node] = True + def getActiveNodes(self): """ Return list of lists with active paths in net, the size of list depends on the number of net outputs. It populates list in reverse @@ -158,7 +176,23 @@ def getCompiledCode(self): compiled = [] for e in expr: compiled.append(compile(e, "", "eval")) - return compiled + return compiled + + def mutate(self, **args): + """ Overloaded method of GenomeBase. Mutators of G2DCartesian return + list of mutated nodes and if any of them is on the list of active + expression nodes then genome flag to reevaluate is set """ + mutated_nodes = [] + for it in self.mutator.applyFunctions(self, **args): + mutated_nodes += it + + for node in mutated_nodes: + if self.expressionNodes.has_key(node): + self.reevaluate = True + return len(mutated_nodes) + + self.reevaluate = False + return len(mutated_nodes) def writeDotGraph(self, graph): """ Populates graph for pydot from active expression of genome """ diff --git a/pyevolve/Mutators.py b/pyevolve/Mutators.py index 95a173e..87ff94a 100644 --- a/pyevolve/Mutators.py +++ b/pyevolve/Mutators.py @@ -1150,15 +1150,17 @@ def G2DCartesianMutatorNodeInputs(genome, **args): genome.inputs) if mutations < 1.0: mutations = 1 - + + mutated = [] for i in xrange(0, int(mutations)): choosen = rand_choice(genome.nodes[genome.inputs:]) + mutated.append(choosen) previous_nodes = genome.nodes[:(genome.cols * choosen.x + genome.inputs)] for idx, input in enumerate(choosen.inputs): choosen.inputs[idx] = rand_choice(previous_nodes) - return int(mutations) + return mutated def G2DCartesianMutatorNodeParams(genome, **args): @@ -1174,13 +1176,15 @@ def G2DCartesianMutatorNodeParams(genome, **args): genome.inputs) if mutations < 1.0: mutations = 1 - + + mutated = [] for i in xrange(0, int(mutations)): choosen = rand_choice(genome.nodes[genome.inputs:-genome.outputs]) + mutated.append(choosen) for key in choosen.params.keys(): choosen.params[key] = eval(CartesianNode.paramMapping[key]) - return int(mutations) + return mutated def G2DCartesianMutatorNodeFunction(genome, **args): """ The mutator of G2DCartesian, Node value mutator @@ -1198,9 +1202,10 @@ def G2DCartesianMutatorNodeFunction(genome, **args): mutations = 1 function_set = ga_engine.getParam("gp_function_set") - + mutated = [] for i in xrange(0, int(mutations)): - choosen = rand_choice(genome.nodes[genome.inputs:-genome.outputs]) + choosen = rand_choice(genome.nodes[genome.inputs:-genome.outputs]) + mutated.append(choosen) choosen.data = rand_choice(function_set.keys()) previous_nodes = genome.nodes[:(genome.cols * choosen.x + genome.inputs)] @@ -1212,7 +1217,7 @@ def G2DCartesianMutatorNodeFunction(genome, **args): for i in xrange(0, -inputs_diff): choosen.inputs.append(rand_choice(previous_nodes)) - return int(mutations) + return mutated def G2DCartesianMutatorNodesOrder(genome, **args): """ The mutator of G2DCartesian, Nodes order in active path mutator @@ -1225,7 +1230,7 @@ def G2DCartesianMutatorNodesOrder(genome, **args): The *G2DCartesianMutatorNodesOrder* function """ paths = genome.getActiveNodes() - mutations = 0 + mutated = [] for path in paths: shuffled_functions = [] for node in path: @@ -1236,14 +1241,14 @@ def G2DCartesianMutatorNodesOrder(genome, **args): new_function = shuffled_functions[idx] previous_nodes = genome.nodes[:(genome.cols * node.x + genome.inputs)] - node.data = new_function[0] + node.data = new_function[0] inputs_diff = len(node.inputs) - (new_function[1]) if inputs_diff > 0: - del node.inputs[-inputs_diff:] + del node.inputs[-inputs_diff:] elif inputs_diff < 0: for i in xrange(0, -inputs_diff): node.inputs.append(rand_choice(previous_nodes)) - mutations += len(path) + mutated.append(node) - return mutations + return mutated diff --git a/tests/test_genomes.py b/tests/test_genomes.py index 49709e0..7334aeb 100644 --- a/tests/test_genomes.py +++ b/tests/test_genomes.py @@ -103,6 +103,65 @@ def test_genome_to_graph(self): graph = pydot.Dot(graph_type='graph') self.genome.writeDotGraph(graph) self.assertTrue(len(graph.get_node_list()) >= self.genome.outputs) + + def test_genome_mutate_expressed(self): + self.genome.expressionNodes = {MagicMock() : True, MagicMock() : True, + MagicMock() : True} + def apply_effect(arg, **args): + mutated = [] + mutated.append([]) + for i in xrange(0, len(self.genome.expressionNodes)): + mutated[0].append(choice(self.genome.expressionNodes.keys())) + return mutated + + self.genome.mutator.applyFunctions = MagicMock( + side_effect = apply_effect) + ret = self.genome.mutate() + self.assertTrue(ret > 0) + self.assertTrue(self.genome.reevaluate) + + def test_genome_mutate_non_expressed(self): + def apply_effect(arg, **args): + mutated = [] + mutated.append([MagicMock(), MagicMock(), MagicMock()]) + return mutated + + self.genome.mutator.applyFunctions = MagicMock( + side_effect = apply_effect) + ret = self.genome.mutate() + self.assertTrue(ret > 0) + self.assertFalse(self.genome.reevaluate) + + def test_genome_evaluate_not(self): + expected_expression_nodes = {MagicMock() : True, MagicMock() : True, + MagicMock() : True} + self.genome.expressionNodes = expected_expression_nodes + self.genome.reevaluate = False + self.genome.evaluate() + self.assertEqual(expected_expression_nodes, self.genome.expressionNodes) + + def test_genome_evaluate_do(self): + nodes_pool = [] + for i in xrange(0, 10): + nodes_pool.append(MagicMock()) + + def get_effect(arg): + ret = [] + for i in xrange(0, randint(1, 5)): + ret.append(choice(nodes_pool)) + arg[:] = list(ret) + + for i in xrange(0, self.outs): + self.genome[-1-i] = MagicMock() + self.genome[-1-i].getPreviousNodes = MagicMock( + side_effect = get_effect) + self.genome.evaluator.applyFunctions = MagicMock( + return_value = [0.1 , 3.0]) + self.genome.reevaluate = True + self.genome.evaluate() + self.assertTrue(len(self.genome.expressionNodes) > 0) + for node in self.genome.expressionNodes.keys(): + self.assertTrue(node in nodes_pool) class CartesianNodeTestCase(unittest.TestCase): @patch('pyevolve.G2DCartesian.rand_randint') diff --git a/tests/test_mutators.py b/tests/test_mutators.py index 9d89309..10653d7 100644 --- a/tests/test_mutators.py +++ b/tests/test_mutators.py @@ -219,8 +219,10 @@ def choice_effect(arg): for i in values[1:]: expected_result.append(self.genome[i]) - Mutators.G2DCartesianMutatorNodeInputs(self.genome, pmut=0.1) + mutations = Mutators.G2DCartesianMutatorNodeInputs(self.genome, + pmut=0.1) self.assertEqual(self.genome[3].inputs, expected_result) + self.assertTrue(self.genome[3] in mutations) @patch('pyevolve.Mutators.rand_choice') def test_cartesian_mutator_inputs_large_pmut(self, rand_mock): @@ -241,9 +243,11 @@ def choice_effect(arg): self.genome[values[idx]]) idx = idx+1 - Mutators.G2DCartesianMutatorNodeInputs(self.genome, pmut=0.7) + mutations = Mutators.G2DCartesianMutatorNodeInputs(self.genome, + pmut=0.7) for key in expected_result.keys(): self.assertEqual(self.genome[key].inputs, expected_result[key]) + self.assertTrue(self.genome[key] in mutations) @patch('pyevolve.Mutators.rand_choice') def test_cartesian_mutator_function_small_pmut(self, rand_mock): @@ -256,10 +260,11 @@ def choice_effect(arg): return what rand_mock.side_effect = choice_effect - Mutators.G2DCartesianMutatorNodeFunction(self.genome, pmut=0.1, - ga_engine = self.ga_engine) + mutations = Mutators.G2DCartesianMutatorNodeFunction(self.genome, + pmut=0.1, ga_engine = self.ga_engine) self.assertEqual(self.genome[4].data, "f3") self.assertEqual(len(self.genome[4].inputs), self.function_set["f3"]-1) + self.assertTrue(self.genome[4] in mutations) @patch('pyevolve.Mutators.rand_choice') def test_cartesian_mutator_function_large_pmut(self, rand_mock): @@ -272,8 +277,8 @@ def choice_effect(arg): return what rand_mock.side_effect = choice_effect - Mutators.G2DCartesianMutatorNodeFunction(self.genome, pmut=0.7, - ga_engine = self.ga_engine) + mutations = Mutators.G2DCartesianMutatorNodeFunction(self.genome, + pmut=0.7, ga_engine = self.ga_engine) values = [1, "f3", 0, 2, "f4", 0, 0, 0, 3, "f1", 4, "f4", 0, 0] idx = 0 while idx >= 0: @@ -283,6 +288,7 @@ def choice_effect(arg): self.assertEqual(self.genome[node_idx].data, func) self.assertEqual(len(self.genome[node_idx].inputs), self.function_set[func]-1) + self.assertTrue(self.genome[node_idx] in mutations) values = values[2:] idx = (next((key for key, val in enumerate(values) if @@ -296,9 +302,11 @@ def test_cartesian_mutator_params_small_pmut(self, rand_mock): for param in CartesianNode.paramMapping.keys(): self.genome[value].params[param] = -1 rand_mock.return_value = self.genome[value] - Mutators.G2DCartesianMutatorNodeParams(self.genome, pmut=0.1) + mutations = Mutators.G2DCartesianMutatorNodeParams(self.genome, + pmut=0.1) for param in self.genome[value].params.values(): self.assertTrue(param in range(0,11)) + self.assertTrue(self.genome[value] in mutations) CartesianNode.paramMapping.clear() @patch('pyevolve.Mutators.rand_choice') @@ -312,11 +320,13 @@ def test_cartesian_mutator_params_large_pmut(self, rand_mock): def choice_effect(arg): return self.genome[values.pop(0)] rand_mock.side_effect = choice_effect - Mutators.G2DCartesianMutatorNodeParams(self.genome, pmut=0.7) + mutations = Mutators.G2DCartesianMutatorNodeParams(self.genome, + pmut=0.7) values = [1, 2, 3, 4] for v in values: for param in self.genome[v].params.values(): self.assertTrue(param in range(0,11)) + self.assertTrue(self.genome[v] in mutations) CartesianNode.paramMapping.clear() @patch('pyevolve.Mutators.rand_shuffle') @@ -331,11 +341,16 @@ def shuffle_effect(arg): return rand_mock.side_effect = shuffle_effect - Mutators.G2DCartesianMutatorNodesOrder(self.genome, pmut=0.2) - paths = self.genome.getActiveNodes() + mutations = Mutators.G2DCartesianMutatorNodesOrder(self.genome, + pmut=0.2) + + paths = self.genome.getActiveNodes() shuffled_functions = [] for node in paths[0]: shuffled_functions.append(node.getData()) + for mutation in mutations: + self.assertTrue(mutation in paths[0]) + for func in expected_order: - self.assertTrue(func in shuffled_functions) \ No newline at end of file + self.assertTrue(func in shuffled_functions) \ No newline at end of file From f052bd4d3e1fcbab331fcd4d5db7e17615456797 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Sun, 10 Jul 2016 00:20:57 +0200 Subject: [PATCH 08/20] VectorErrorAccumulator addon New object in utils for better array-like error calculation. Based on ErrorAccumulator, basic tests included. --- .gitignore | 2 ++ pyevolve/Util.py | 63 ++++++++++++++++++++++++++++++++++++-- tests/test_util.py | 76 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d81d41e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.coverage +*.pyc \ No newline at end of file diff --git a/pyevolve/Util.py b/pyevolve/Util.py index 4674797..2462d16 100644 --- a/pyevolve/Util.py +++ b/pyevolve/Util.py @@ -180,8 +180,8 @@ def append(self, target, evaluated): def __iadd__(self, value): """ The same as append, but you must pass a tuple """ self.append(*value) - return self - + return self + def getMean(self): """ Return the mean of the non-squared accumulator """ return self.acc / self.acc_len @@ -214,6 +214,65 @@ def getMSE(self): """ return self.acc_square / float(self.acc_len) +try: + import numpy as np + + class VectorErrorAccumulator(ErrorAccumulator): + """ Vector version of error accumulator, it uses numpy and it is natural + choice for analysing signal data (e.g. images). Appending one by one - + result and target from fitness function, is slower than passing it via + numpy arrays. Numpy also offers some methods usuable for signal + comparing, especially in making confusion matrix. + """ + def __init__(self, confusion = True, non_zeros = True): + super(VectorErrorAccumulator, self).__init__() + self.non_zeros = 0 + self.confusion = np.zeros([2, 2]) + self.calculate_confusion = confusion + self.calculate_non_zeros = non_zeros + + def reset(self): + """ Reset the accumulator """ + super(VectorErrorAccumulator, self).reset() + self.confusion = np.zeros([2,2]) + self.non_zeros = 0 + + def append(self, target, evaluated): + """ Add array value to the accumulator + + :param target: the array target value + :param evaluated: the array evaluated value + """ + diff = target - evaluated + + if self.calculate_confusion: + targetBin = np.ravel(np.where(target > 0, 1, 0)) + evaluatedBin = np.ravel(np.where(evaluated > 0, 1, 0)) + flags=np.logical_not(np.bitwise_xor(targetBin,evaluatedBin)) + self.confusion = np.bincount(2 * flags + evaluatedBin, + minlength=4).reshape(2, 2).astype(np.float64) + + if self.calculate_non_zeros: + self.non_zeros = np.count_nonzero(diff) + + self.acc_square += np.sum((diff)**2) + self.acc += np.sum(np.absolute(diff)) + self.acc_len += target.size + + def getConfusionMatrix(self): + """ Return the confusion matrix of accumulator """ + return self.confusion + + def getNonZeros(self): + """ Return the counter of non zero values in the accumulator """ + return self.non_zeros + + def getZeros(self): + """ Return the counter of zero values in the accumulator """ + return self.acc_len - self.non_zeros + +except: + print 'No numpy module found, VectorErrorAccumulator class is unavailable.' class Graph(object): """ The Graph class diff --git a/tests/test_util.py b/tests/test_util.py index dea3a4d..63f57e7 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -16,4 +16,78 @@ def test_randomFlipCoin_border_cases(self): def test_list2DSwapElement(self): _list = [[1, 2, 3], [4, 5, 6]] Util.list2DSwapElement(_list, (0, 1), (1, 1)) - self.assertEqual(_list, [[1, 5, 3], [4, 2, 6]]) \ No newline at end of file + self.assertEqual(_list, [[1, 5, 3], [4, 2, 6]]) + +try: + import numpy as np + + class VectorErrorAccumulatorTestCase(TestCase): + def setUp(self): + np.empty([2, 2]) + self.VEC = Util.VectorErrorAccumulator(False, False) + self.VECC = Util.VectorErrorAccumulator(True, False) + self.VECZ = Util.VectorErrorAccumulator(False, True) + self.VECCZ = Util.VectorErrorAccumulator(True, True) + self.target = np.ones([12, 12]) + self.evaluated = np.zeros([12, 12]) + + def test_init_standard(self): + self.assertTrue(self.VEC.non_zeros == 0) + self.assertTrue((self.VEC.confusion == np.zeros([2, 2])).all()) + + def test_init_without_any(self): + self.assertTrue(self.VEC.calculate_confusion == False) + self.assertTrue(self.VEC.calculate_non_zeros == False) + + def test_init_with_confusion(self): + self.assertTrue(self.VECC.calculate_confusion == True) + self.assertTrue(self.VECC.calculate_non_zeros == False) + + def test_init_with_confusion_and_zeros(self): + self.assertTrue(self.VECCZ.calculate_confusion == True) + self.assertTrue(self.VECCZ.calculate_non_zeros == True) + + def test_init_with_zeros(self): + self.assertTrue(self.VECZ.calculate_confusion == False) + self.assertTrue(self.VECZ.calculate_non_zeros == True) + + + def test_reset_empty(self): + self.VEC.reset() + self.assertTrue(self.VEC.non_zeros == 0) + self.assertTrue((self.VEC.confusion == np.zeros([2, 2])).all()) + + def test_reset_non_empty(self): + self.VEC.non_zeros = 10 + self.VEC.confusion = np.ones([2 ,2]) + self.VEC.reset() + self.assertTrue(self.VEC.non_zeros == 0) + print self.VEC.confusion + self.assertTrue((self.VEC.confusion == np.zeros([2, 2])).all()) + + def test_append_to_empty(self): + self.VEC.append(self.target, self.evaluated) + self.assertTrue(self.VEC.acc_len == 144) + + def test_append_to_non_empty(self): + self.VEC.append(self.target, self.evaluated) + self.VEC.append(self.target, self.evaluated) + self.assertTrue(self.VEC.acc_len == 144*2) + + def test_get_confusion_matrix(self): + self.VECC.append(self.target, self.evaluated) + ret = self.VECC.getConfusionMatrix() + self.assertTrue((np.array([[144, 0], [0, 0]]) == ret).all()) + + def test_get_zeros(self): + self.VECZ.append(self.target, self.evaluated) + ret = self.VECZ.getZeros() + self.assertTrue(ret == 0) + + def test_get_non_zeros(self): + self.VECCZ.append(self.target, self.evaluated) + ret = self.VECCZ.getNonZeros() + self.assertTrue(ret == 144) + +except: + pass \ No newline at end of file From 6bef6235bc64cf1ede18c6c9b7d5c3cecd7fb767 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Sun, 10 Jul 2016 12:14:19 +0200 Subject: [PATCH 09/20] Multithreading evaluation Added possibility of using multithreading in evaluation of individuals from population. Multithreading is memory cheaper and faster in initialization than multiprocessing. Unit tests included. --- .gitignore | 2 ++ pyevolve/GPopulation.py | 55 +++++++++++++++++++++++++++++++++++++++- tests/test_population.py | 37 +++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 tests/test_population.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d81d41e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.coverage +*.pyc \ No newline at end of file diff --git a/pyevolve/GPopulation.py b/pyevolve/GPopulation.py index 919ad18..93d80a9 100644 --- a/pyevolve/GPopulation.py +++ b/pyevolve/GPopulation.py @@ -47,6 +47,15 @@ except ImportError: MULTI_PROCESSING = False logging.debug("You don't have multiprocessing support for your Python version !") + +try: + from threading import Thread + CPU_COUNT = cpu_count() + MULTI_THREADING = True if CPU_COUNT > 1 else False + logging.debug("You have %d CPU cores, so the threading state is %s", CPU_COUNT, MULTI_PROCESSING) +except ImportError: + MULTI_THREADING = False + logging.debug("You don't have threading support for your Python version !") def key_raw_score(individual): @@ -134,6 +143,7 @@ def __init__(self, genome): self.internalParams = genome.internalParams self.multiProcessing = genome.multiProcessing + self.multiThreading = genome.multiThreading self.statted = False self.stats = Statistics() @@ -153,6 +163,7 @@ def __init__(self, genome): self.internalParams = {} self.multiProcessing = (False, False, None) + self.multiThreading = (False, None) # Statistics self.statted = False @@ -179,6 +190,23 @@ def setMultiProcessing(self, flag=True, full_copy=False, max_processes=None): """ self.multiProcessing = (flag, full_copy, max_processes) + + def setMultiThreading(self, flag=True, max_threads=None): + """ Sets the flag to enable/disable the use of python multithreading module. + Use this option when you have more than one core on your CPU and when your + evaluation function is very slow and can use the same non mutuable target + to evaluate fitness (e.g. images). Threading seems to be more efficient + than processing, there is no need of copying resources. Thread just points + the method and data in existing memory to execute but remember that + threads share memory space. + + :param flag: True (default) or False + :param max_threads: None (default) or an integer value + + + """ + + self.multiThreading = (flag, max_threads) def setMinimax(self, minimax): """ Sets the population minimax @@ -383,8 +411,33 @@ def evaluate(self, **args): :param args: this params are passed to the evaluation function """ + + # We have multithreading + if self.multiThreading[0] and MULTI_THREADING: + logging.debug("Evaluating the population using the" + "multithreading method") + t = {} + if self.multiThreading[1] == None: + spawn_size = len(self) + else: + spawn_size = self.multiThreading[1] + + for counter in xrange(0, len(self), spawn_size): + t.clear() + is_overflow = (counter + spawn_size) > len(self) + overflow_size = is_overflow * (spawn_size+counter - len(self)) + start = counter + end = counter + spawn_size - overflow_size + for ind, idx in zip(self.internalPop[start:end], + xrange(0, spawn_size-overflow_size)): + t[idx] = Thread(target=ind.evaluate(), args=(ind, )) + t[idx].start() + + for idx in range(0, spawn_size-overflow_size): + t[idx].join() + # We have multiprocessing - if self.multiProcessing[0] and MULTI_PROCESSING: + elif self.multiProcessing[0] and MULTI_PROCESSING: logging.debug("Evaluating the population using the multiprocessing method") proc_pool = Pool(processes=self.multiProcessing[2]) diff --git a/tests/test_population.py b/tests/test_population.py new file mode 100644 index 0000000..59f7cb3 --- /dev/null +++ b/tests/test_population.py @@ -0,0 +1,37 @@ +from unittest import TestCase +from mock import MagicMock +from pyevolve.GPopulation import GPopulation + + +class MultithreadingTestCase(TestCase): + def setUp(self): + + self.scores = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1] + def eval_effect(idx): + self.population.internalPop[idx].score = self.scores[idx] + + self.mockGenome = MagicMock() + self.population = GPopulation(self.mockGenome) + self.population.internalPop = [MagicMock() for _ in xrange(0,11)] + for idx, mock in enumerate(self.population.internalPop): + mock.score = 0.0 + mock.evaluate = MagicMock(side_effect = eval_effect(idx)) + + def test_init(self): + self.assertTrue(self.population.multiThreading == (False, None)) + + def test_set_MT(self): + self.population.setMultiThreading(True, 10) + self.assertTrue(self.population.multiThreading == (True, 10)) + + def test_run_MT_without_constraint(self): + self.population.setMultiThreading(True) + self.population.evaluate() + for ind in self.population.internalPop: + self.assertTrue(ind.score in self.scores) + + def test_run_MT_with_constraints(self): + self.population.setMultiThreading(True, 3) + self.population.evaluate() + for ind in self.population.internalPop: + self.assertTrue(ind.score in self.scores) \ No newline at end of file From b3504eddec4f4b228784b66ffcc6a66d10494179 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Sun, 10 Jul 2016 12:15:07 +0200 Subject: [PATCH 10/20] Missing comment modification --- pyevolve/GPopulation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyevolve/GPopulation.py b/pyevolve/GPopulation.py index 93d80a9..2530608 100644 --- a/pyevolve/GPopulation.py +++ b/pyevolve/GPopulation.py @@ -203,6 +203,8 @@ def setMultiThreading(self, flag=True, max_threads=None): :param flag: True (default) or False :param max_threads: None (default) or an integer value + .. versionadded:: + The `setMultiThreading` method. """ From 160362cd9c16e257b7a6e2ca9a081dbc58d1cbfd Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Tue, 12 Jul 2016 19:27:16 +0200 Subject: [PATCH 11/20] Improved migration Changed sending-receiving const chunk size schema. Now first 4 bytes of message is size of the data and sending-receiving is looped until all bytes from data are sent. This change allows to send bigger genomes (CGP genomes can have much bigger size) also after individual migration their score is reevaluated (every island can have different fitness function). Added tests for sending-receiving changes. --- .gitignore | 2 ++ pyevolve/Migration.py | 4 ++++ pyevolve/Network.py | 23 ++++++++++++++++++++--- tests/test_network.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 .gitignore create mode 100644 tests/test_network.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d81d41e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.coverage +*.pyc \ No newline at end of file diff --git a/pyevolve/Migration.py b/pyevolve/Migration.py index 3f566c9..07bd88f 100644 --- a/pyevolve/Migration.py +++ b/pyevolve/Migration.py @@ -263,6 +263,8 @@ def exchange(self): if len(pool) <= 0: break choice = rand_choice(pool) + # reevaluate choice, sometimes we use different methods on islands + choice[2].evaluate() pool.remove(choice) # replace the worst @@ -333,6 +335,8 @@ def exchange(self): break choice = rand_choice(pool) + # reevaluate choice, sometimes we use different methods on islands + choice[2].evaluate() pool.remove(choice) # replace the worst diff --git a/pyevolve/Network.py b/pyevolve/Network.py index 726c82e..e6f4477 100644 --- a/pyevolve/Network.py +++ b/pyevolve/Network.py @@ -232,8 +232,16 @@ def send(self, data): """ bytes = -1 for destination in self.target: - bytes = self.sock.sendto(data, destination) - return bytes + totalsent = 0 + data_copy = data[:] + data = format(len(data), "#06x") + data + to_sent = len(data) + while totalsent < to_sent: + bytes = self.sock.sendto(data, destination) + totalsent += bytes + data = data[bytes:] + data = data_copy[:] + return totalsent def run(self): """ Method called when you call *.start()* of the thread """ @@ -354,8 +362,17 @@ def getData(self): :rtype: tuple (sender ip, data) or None when timeout exception """ + received = 0 + length = 1 + data = "" try: - data, sender = self.sock.recvfrom(self.bufferSize) + while received < length: + buffer, sender = self.sock.recvfrom(self.bufferSize) + if received == 0: + length = int(buffer[:6], 16) + buffer = buffer[6:] + received += len(buffer) + data += buffer except socket.timeout: return None return (sender[0], data) diff --git a/tests/test_network.py b/tests/test_network.py new file mode 100644 index 0000000..5097306 --- /dev/null +++ b/tests/test_network.py @@ -0,0 +1,43 @@ +from unittest import TestCase + +from mock import MagicMock +from pyevolve import Network + +class UDPThreadServerTestCase(TestCase): + def setUp(self): + self.server = Network.UDPThreadServer("localhost", 777) + self.plain_data = ("living is not enough one must have sunshine freedom" + "and a little flower") + self.data = format(len(self.plain_data), "#06x") + self.plain_data + self.chunk_size = 8 + + def test_get_data_packs(self): + def recv_effect(size): + buf = self.data[:self.chunk_size] + self.data = self.data[self.chunk_size:] + return (buf, ("Butterfly", )) + + self.server.sock = MagicMock() + self.server.sock.recvfrom = MagicMock(side_effect = recv_effect) + + ret = self.server.getData() + self.assertEqual(ret, ("Butterfly", self.plain_data)) + +class UDPThreadUnicastClientTestCase(TestCase): + def setUp(self): + self.client = Network.UDPThreadUnicastClient("localhost", 777) + self.plain_data = ("living is not enough one must have sunshine freedom" + "and a little flower") + self.client.target = 4*[None] + self.chunk_size = 8 + + def test_send_data_packs(self): + def sendto_effect(data, dest): + buf = data[:self.chunk_size] + return len(buf) + + self.client.sock = MagicMock() + self.client.sock.sendto = MagicMock(side_effect = sendto_effect) + + ret = self.client.send(self.plain_data) + self.assertEqual(ret, len(self.plain_data) + 6) \ No newline at end of file From 805d843691ad93c3c05a076d456d3ffe848fe813 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Mon, 18 Jul 2016 18:16:42 +0200 Subject: [PATCH 12/20] Missing setter for GSimpleGA Added missing GA's method for setting multithreading, which calls internal population method for MT. --- pyevolve/GSimpleGA.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pyevolve/GSimpleGA.py b/pyevolve/GSimpleGA.py index 44775d5..76dfc7e 100644 --- a/pyevolve/GSimpleGA.py +++ b/pyevolve/GSimpleGA.py @@ -427,6 +427,41 @@ def setMultiProcessing(self, flag=True, full_copy=False, max_processes=None): self.internalPop.setMultiProcessing(flag, full_copy, max_processes) + def setMultiThreading(self, flag=True, max_threads=None): + """ Sets the flag to enable/disable the use of python multithreading module. + Use this option when you have more than one core on your CPU and when your + evaluation function is very slow. + + Pyevolve will automaticly check if your Python version has **multithreading** + support and if you have more than one single CPU core. If you don't have support + or have just only one core, Pyevolve will not use the **multithreading** + feature. + + Pyevolve uses the **multithreading** to execute the evaluation function over + the individuals, so the use of this feature will make sense if you have a + truly slow evaluation function (which is commom in GAs). + + Multithreading in general is better than multiprocessing when target data + to compare is big and copying it during process initialization is time + expensive. + + :param flag: True (default) or False + :param max_threads: None (default) or an integer value + + .. warning:: Use this option only when your evaluation function is slow, so you'll + get a good tradeoff between the process communication speed and the + parallel evaluation. The use of the **multithreading** doesn't means + always a better performance. + + .. versionadded:: + The `setMultiThreading` method. + + """ + if type(flag) != BooleanType: + Util.raiseException("Threading option must be True or False", TypeError) + + self.internalPop.setMultiThreading(flag, max_threads) + def setMigrationAdapter(self, migration_adapter=None): """ Sets the Migration Adapter From 6bac31adcaffef2a4561ab18b3c4ae4bb2406a90 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Mon, 18 Jul 2016 18:19:16 +0200 Subject: [PATCH 13/20] Bug fixes Two bug fixes: 1. Missing multiThreading value assignment in GPopulation copy method. 2. Changed target for thread creation (it was called during creation!) --- pyevolve/GPopulation.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyevolve/GPopulation.py b/pyevolve/GPopulation.py index 2530608..b985bf4 100644 --- a/pyevolve/GPopulation.py +++ b/pyevolve/GPopulation.py @@ -418,25 +418,24 @@ def evaluate(self, **args): if self.multiThreading[0] and MULTI_THREADING: logging.debug("Evaluating the population using the" "multithreading method") - t = {} + current_threads = {} if self.multiThreading[1] == None: spawn_size = len(self) else: spawn_size = self.multiThreading[1] for counter in xrange(0, len(self), spawn_size): - t.clear() + current_threads.clear() is_overflow = (counter + spawn_size) > len(self) overflow_size = is_overflow * (spawn_size+counter - len(self)) start = counter end = counter + spawn_size - overflow_size for ind, idx in zip(self.internalPop[start:end], xrange(0, spawn_size-overflow_size)): - t[idx] = Thread(target=ind.evaluate(), args=(ind, )) - t[idx].start() - - for idx in range(0, spawn_size-overflow_size): - t[idx].join() + current_threads[idx] = Thread(target=ind.evaluate, args=()) + + [thread.start() for thread in current_threads.values()] + [thread.join() for thread in current_threads.values()] # We have multiprocessing elif self.multiProcessing[0] and MULTI_PROCESSING: @@ -506,6 +505,7 @@ def copy(self, pop): pop.scaleMethod = self.scaleMethod pop.internalParams = self.internalParams pop.multiProcessing = self.multiProcessing + pop.multiThreading = self.multiThreading def getParam(self, key, nvl=None): """ Gets an internal parameter From a06e80d6f3b1ce409d9c690cfabf87eab3cdca3e Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Mon, 18 Jul 2016 18:23:53 +0200 Subject: [PATCH 14/20] CGP Genome problem fixed Changed dictionary keying from node object to node position (position is stable, objects not). Added missing expressionNodes copying in genome copy method. --- pyevolve/G2DCartesian.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyevolve/G2DCartesian.py b/pyevolve/G2DCartesian.py index 1f7e60f..805517d 100644 --- a/pyevolve/G2DCartesian.py +++ b/pyevolve/G2DCartesian.py @@ -143,6 +143,8 @@ def copy(self, g): """ GenomeBase.copy(self, g) + g.expressionNodes = self.expressionNodes.copy() + g.reevaluate = self.reevaluate g.nodes = copy.deepcopy(self.nodes) def evaluate(self, **args): @@ -153,9 +155,8 @@ def evaluate(self, **args): super(G2DCartesian, self).evaluate(**args) self.expressionNodes.clear() for path in self.getActiveNodes(): - print path for node in path: - self.expressionNodes[node] = True + self.expressionNodes[(node.x, node.y)] = True def getActiveNodes(self): """ Return list of lists with active paths in net, the size of list @@ -187,7 +188,7 @@ def mutate(self, **args): mutated_nodes += it for node in mutated_nodes: - if self.expressionNodes.has_key(node): + if self.expressionNodes.has_key((node.x, node.y)): self.reevaluate = True return len(mutated_nodes) From dd5bea7aec0c10237ac288896a5a495ce983bd5f Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Mon, 18 Jul 2016 18:44:54 +0200 Subject: [PATCH 15/20] Fixed tests Updating tests after changing expressionNodes keying in CGP genome. --- tests/test_genomes.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/test_genomes.py b/tests/test_genomes.py index 7334aeb..72fa048 100644 --- a/tests/test_genomes.py +++ b/tests/test_genomes.py @@ -104,14 +104,20 @@ def test_genome_to_graph(self): self.genome.writeDotGraph(graph) self.assertTrue(len(graph.get_node_list()) >= self.genome.outputs) - def test_genome_mutate_expressed(self): - self.genome.expressionNodes = {MagicMock() : True, MagicMock() : True, - MagicMock() : True} + def test_genome_mutate_expressed(self): + nodes = [] + for i in xrange(0, randint(2, 5)): + node = MagicMock() + node.x = randint(0, 7) + node.y = randint(0, 7) + self.genome.expressionNodes[(node.x, node.y)] = True + nodes.append(node) + def apply_effect(arg, **args): mutated = [] mutated.append([]) for i in xrange(0, len(self.genome.expressionNodes)): - mutated[0].append(choice(self.genome.expressionNodes.keys())) + mutated[0].append(choice(nodes)) return mutated self.genome.mutator.applyFunctions = MagicMock( @@ -142,13 +148,19 @@ def test_genome_evaluate_not(self): def test_genome_evaluate_do(self): nodes_pool = [] + coords_pool = [] for i in xrange(0, 10): - nodes_pool.append(MagicMock()) + node = MagicMock() + node.x = randint(0, 4) + node.y = randint(0, 4) + nodes_pool.append(node) def get_effect(arg): ret = [] for i in xrange(0, randint(1, 5)): - ret.append(choice(nodes_pool)) + node = choice(nodes_pool) + ret.append(node) + coords_pool.append((node.x, node.y)) arg[:] = list(ret) for i in xrange(0, self.outs): @@ -161,7 +173,7 @@ def get_effect(arg): self.genome.evaluate() self.assertTrue(len(self.genome.expressionNodes) > 0) for node in self.genome.expressionNodes.keys(): - self.assertTrue(node in nodes_pool) + self.assertTrue(node in coords_pool) class CartesianNodeTestCase(unittest.TestCase): @patch('pyevolve.G2DCartesian.rand_randint') From 76be27f1d147b8be67f99eda7a23a24435533e41 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Mon, 18 Jul 2016 18:46:21 +0200 Subject: [PATCH 16/20] Tests port change Changed port value in tests for non-admin system users. --- tests/test_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_network.py b/tests/test_network.py index 5097306..7698a6e 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -5,7 +5,7 @@ class UDPThreadServerTestCase(TestCase): def setUp(self): - self.server = Network.UDPThreadServer("localhost", 777) + self.server = Network.UDPThreadServer("localhost", 1777) self.plain_data = ("living is not enough one must have sunshine freedom" "and a little flower") self.data = format(len(self.plain_data), "#06x") + self.plain_data @@ -25,7 +25,7 @@ def recv_effect(size): class UDPThreadUnicastClientTestCase(TestCase): def setUp(self): - self.client = Network.UDPThreadUnicastClient("localhost", 777) + self.client = Network.UDPThreadUnicastClient("localhost", 1777) self.plain_data = ("living is not enough one must have sunshine freedom" "and a little flower") self.client.target = 4*[None] From 110c8523d7a9f6316bf4f131d330602e2c146531 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Thu, 21 Jul 2016 18:52:30 +0200 Subject: [PATCH 17/20] CGP reevaluation problem repaired Added out nodes to expressionNodes in CGP genome so the reevaluating schema is now correct. --- pyevolve/G2DCartesian.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyevolve/G2DCartesian.py b/pyevolve/G2DCartesian.py index 805517d..a59226c 100644 --- a/pyevolve/G2DCartesian.py +++ b/pyevolve/G2DCartesian.py @@ -154,9 +154,11 @@ def evaluate(self, **args): if self.reevaluate: super(G2DCartesian, self).evaluate(**args) self.expressionNodes.clear() - for path in self.getActiveNodes(): + for idx, path in enumerate(self.getActiveNodes()): for node in path: - self.expressionNodes[(node.x, node.y)] = True + self.expressionNodes[(node.x, node.y)] = True + out = self.nodes[-idx-1] + self.expressionNodes[(out.x, out.y)] = True def getActiveNodes(self): """ Return list of lists with active paths in net, the size of list @@ -183,6 +185,7 @@ def mutate(self, **args): """ Overloaded method of GenomeBase. Mutators of G2DCartesian return list of mutated nodes and if any of them is on the list of active expression nodes then genome flag to reevaluate is set """ + self.reevaluate = False mutated_nodes = [] for it in self.mutator.applyFunctions(self, **args): mutated_nodes += it @@ -192,7 +195,6 @@ def mutate(self, **args): self.reevaluate = True return len(mutated_nodes) - self.reevaluate = False return len(mutated_nodes) def writeDotGraph(self, graph): @@ -203,10 +205,10 @@ def writeDotGraph(self, graph): node_dict = {} node_stack.append(self.nodes[-1-out]) while node_stack: - current_node = node_stack.pop() + current_node = node_stack.pop() if current_node.data != "": node_label = current_node.data - for param in current_node.params.keys(): + for param in current_node.params.keys(): node_label += " " + str(current_node.params[param]) graph.add_node(pydot.Node(str(node_counter), label = node_label)) @@ -264,11 +266,11 @@ def __init__(self, position_x, position_y, data_set = {}, """ The initializator of CartesianNode representation, position_x and position_y must be specified, data_set and previous_nodes depends on type of the node """ - self.data = None - self.inputs = [] - self.params = {} + self.data = None + self.inputs = [] + self.params = {} self.x = position_x - self.y = position_y + self.y = position_y try: self.data = rand_choice(data_set.keys()) From 0bff186cbbdec04d2750197ccbe46d7bcda4ff05 Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Thu, 21 Jul 2016 21:13:04 +0200 Subject: [PATCH 18/20] Util - import error change Now numpy import error is notified via logging module. --- pyevolve/Util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyevolve/Util.py b/pyevolve/Util.py index 2462d16..88d56cd 100644 --- a/pyevolve/Util.py +++ b/pyevolve/Util.py @@ -272,7 +272,8 @@ def getZeros(self): return self.acc_len - self.non_zeros except: - print 'No numpy module found, VectorErrorAccumulator class is unavailable.' + logging.info('No numpy module found, VectorErrorAccumulator class is ' + 'unavailable.') class Graph(object): """ The Graph class From b94e91c13d8c1f06a46e499e98155c9eb1dd704d Mon Sep 17 00:00:00 2001 From: ImpactHorizon Date: Sat, 23 Jul 2016 13:39:03 +0200 Subject: [PATCH 19/20] CGP example Added CGP example file. Finding image filter schema to obtain target data from input data. Using params in nodes, multithreading and VectorErrorAccumulator for better presentation of working with signals idea. --- examples/data/input.jpg | Bin 0 -> 150572 bytes examples/data/target.jpg | Bin 0 -> 22521 bytes examples/pyevolve_ex23_cgp.py | 163 ++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 examples/data/input.jpg create mode 100644 examples/data/target.jpg create mode 100644 examples/pyevolve_ex23_cgp.py diff --git a/examples/data/input.jpg b/examples/data/input.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f51549de29a814e19027842150d1ea4bdab7b3c GIT binary patch literal 150572 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%0DezC-{0%3qA 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 BEY35IpGM zgrGqaAaZk)+`IYx|699N`)YSjSNEGf)%E>Ucbz`Z_dNft|F=iRYG!0=L`F_dMn?YU zAp38NOrMO7hK81gnvRy1mY$xDfr*ouiII_shn?dN=RMy00{3|N`2|JgB?N_KMfmw8 zRi$JV6oEkCeF?CZnv$lxGEnJ%9zsq}PtU~2#Ldjitt7-Rr1bwe{u?1Q8pU zpZjD~Y}D*Rin=r$wjQ*?;haiI`89MRdL5$xyT#9<%AOI)^bA~gxp{cS#3dx9q=6t6 zRW-1>zJZ~Uv5Bdfy#oa5=;RFZ^7ird^AA8oMn%WOK8}k|Nli~0PtUGzZoho} z_Wkc4|NIXZIT_{u#`<&p-(dd_F19~h6jW4{RJ8xYMNSd>=cHt#q83u5Vb`^#^$6z> zR!XAd)XT5w7^N3cw)+h5j96sg5(Rz`yZ#@v|3&uy4lMcq7uo*>_J4D2k}*+||9N?o zY-A6~HW$7;kqR19<7s*DWkzkCni61c{EFG1q8F6#q1b7rUA!9!rE%X4$kTi&eQlAY z-y5b6{g|rob7;kGOMNd_T9s0XS%BKJCZIRo+hizLrY3hql*Zx~`7U&~!|~K^b$zy6 zPMBUf$yq(OmhcoC?$Yo(DR4vtjS5zomP~f{#mzh{mz1)Z z3tAf9^Iu??YjVyQwo$e*tRD*-=fr0E2@hL^!&rx64rIX?Q<_Hek1=`1#Q@Lu#@&_V zl*o>l*Zvl5X6U)6%?wF%Dn;XZ16rxUz1d4Im^TMQ7%c;x>nju_@KN7vse7pZNiucv z
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ϕ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 Date: Sun, 2 Nov 2025 19:02:07 +0100 Subject: [PATCH 20/20] most recent code from drive --- examples/pyevolve_ex23_cgp.py | 21 +++++++------ examples/test.py | 55 +++++++++++++++++++++++++++++++++++ pyevolve/GPopulation.py | 3 +- pyevolve/GSimpleGA.py | 5 ++-- 4 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 examples/test.py diff --git a/examples/pyevolve_ex23_cgp.py b/examples/pyevolve_ex23_cgp.py index fdf5302..9c22706 100644 --- a/examples/pyevolve_ex23_cgp.py +++ b/examples/pyevolve_ex23_cgp.py @@ -84,7 +84,7 @@ def gp_mode_filter(src, params): return src.filter(ImageFilter.ModeFilter(params['pos_int'])) def eval_fitness_mean_diff(chromosome): - rmse_accum = Util.VectorErrorAccumulator() + rmse_accum = Util.VectorErrorAccumulator() code = chromosome.getCompiledCode() evaluated = np.array(eval(code[0])) rmse_accum.append(evaluated, target) @@ -121,24 +121,23 @@ def main(): kernel = """ [rand_uniform(0.1, 2.5) for x in range(0,25) ] """ kernel_size_rad = """ rand_choice([3, 5]) """ - random.seed(datetime.now()) + random.seed(13) global im, target, tfft orig = Image.open(TARGET) target = np.array(orig) im = Image.open(INPUT) - im.load() - - genome = G2DCartesian.G2DCartesian(32, 3, 1, 1) + im.load() + genome = G2DCartesian.G2DCartesian(128, 3, 1, 1) genome.evaluator += eval_fitness_mean_diff - ga = GSimpleGA.GSimpleGA(genome) + ga = GSimpleGA.GSimpleGA(genome, seed=13) genome.mutator.set(Mutators.G2DCartesianMutatorNodeParams) genome.mutator.add(Mutators.G2DCartesianMutatorNodeInputs) genome.mutator.add(Mutators.G2DCartesianMutatorNodeFunction) genome.mutator.add(Mutators.G2DCartesianMutatorNodesOrder) - ga.setPopulationSize(5) - ga.setGenerations(10000) - ga.setMultiThreading(True) + ga.setPopulationSize(8) + ga.setGenerations(200) + #ga.setMultiThreading(True) ga.setMinimax(Consts.minimaxType["minimize"]) ga.setParams(gp_function_prefix = "gp", gp_terminals = ['im'], gp_args_mapping = { "pos_int" : pos_int, @@ -154,10 +153,10 @@ def main(): ga.stepCallback.set(step_callback) ga(freq_stats=100) - + end = datetime.now() best = ga.bestIndividual() store_result(best, "best_%s.png" % (best.score)) - store_graph(best, "best_graph_%s.png" % (best.score)) + store_graph(best, "best_graph_%s.png" % (best.score)) if __name__ == "__main__": main() diff --git a/examples/test.py b/examples/test.py new file mode 100644 index 0000000..02d279a --- /dev/null +++ b/examples/test.py @@ -0,0 +1,55 @@ +from pyevolve import Util +from PIL import Image +import numpy as np +import random +import pywt +import math + +IMG_WIDTH=608 +IMG_HEIGHT=300 + +w=Image.new("L", (4,4), "white") +pix=w.load() + +#for i in range(0,4): +# for j in range(0,4): +# pix[i,j] = (random.randint(0,255), random.randint(0,255), random.randint(0,255)) + +b=Image.new("L", (4,4), "black") +b=np.array(b) +orig = Image.open("./data/est.png") +target = np.array(orig) +input = np.array(Image.open("./data/target.jpg")) +tfft = np.fft.fft2(target) +rmse_accum = Util.VectorErrorAccumulator() + + + +efft = np.fft.fft2(input) +#print abs(efft/len(efft)) +#print abs(tfft/len(tfft)) +rmse_accum.append(abs(tfft/len(tfft)), abs(efft/len(efft))) +print rmse_accum.getMean()/255 + +rmse_accum.reset() +rmse_accum.append(target, input) +print rmse_accum.getRMSE() +score = 0 +for i in range(0,3): + ev = input[:,:,i] + hist = np.histogram2d(ev.ravel(), target[:,:,i].ravel(), [x for x in xrange(0, 256)])[0] + nonzeroInd = np.nonzero(hist) + nonzero = hist[nonzeroInd] + histProb = nonzero/float(608*300) + score += -np.sum(np.log2(histProb)*histProb) +max_entropy = 3*math.log(IMG_WIDTH*IMG_HEIGHT, 2) +print score/max_entropy + +#cA, (cH, cV, cD) = pywt.dwt2(target, 'haar') +#print cA +#print cD +#cA, (cH, cV, cD) = pywt.dwt2(b, 'haar') +#print cA +#print cD +#print pywt.dwt2(b, 'haar') + \ No newline at end of file diff --git a/pyevolve/GPopulation.py b/pyevolve/GPopulation.py index b985bf4..8436996 100644 --- a/pyevolve/GPopulation.py +++ b/pyevolve/GPopulation.py @@ -413,7 +413,6 @@ def evaluate(self, **args): :param args: this params are passed to the evaluation function """ - # We have multithreading if self.multiThreading[0] and MULTI_THREADING: logging.debug("Evaluating the population using the" @@ -423,7 +422,7 @@ def evaluate(self, **args): spawn_size = len(self) else: spawn_size = self.multiThreading[1] - + for counter in xrange(0, len(self), spawn_size): current_threads.clear() is_overflow = (counter + spawn_size) > len(self) diff --git a/pyevolve/GSimpleGA.py b/pyevolve/GSimpleGA.py index 76dfc7e..a0d451f 100644 --- a/pyevolve/GSimpleGA.py +++ b/pyevolve/GSimpleGA.py @@ -717,6 +717,7 @@ def step(self): logging.debug("Evaluating the new created population.") newPop.evaluate() + newPop.sort() if self.elitism: logging.debug("Doing elitism.") @@ -724,13 +725,13 @@ def step(self): for i in xrange(self.nElitismReplacement): #re-evaluate before being sure this is the best self.internalPop.bestRaw(i).evaluate() - if self.internalPop.bestRaw(i).score > newPop.bestRaw(i).score: + if self.internalPop.bestRaw(i).score > newPop[len(newPop) - 1 - i].score: newPop[len(newPop) - 1 - i] = self.internalPop.bestRaw(i) elif self.getMinimax() == Consts.minimaxType["minimize"]: for i in xrange(self.nElitismReplacement): #re-evaluate before being sure this is the best self.internalPop.bestRaw(i).evaluate() - if self.internalPop.bestRaw(i).score < newPop.bestRaw(i).score: + if self.internalPop.bestRaw(i).score < newPop[len(newPop) - 1 - i].score: newPop[len(newPop) - 1 - i] = self.internalPop.bestRaw(i) self.internalPop = newPop