eckity.genetic_encodings.gp.tree.tree_individual

This module implements the tree class.

  1"""
  2This module implements the tree class.
  3"""
  4
  5import numpy as np
  6from numbers import Number
  7from random import randint, uniform, random
  8
  9from eckity.base.utils import arity
 10from eckity.genetic_encodings.gp.tree.utils import _generate_args
 11
 12from eckity.individual import Individual
 13from eckity.genetic_encodings.gp.tree.functions import f_add, f_sub, f_mul, f_div
 14
 15
 16class Tree(Individual):
 17    """
 18    A tree optimized for genetic programming operations.
 19    It is represented by a list of nodes in depth-first order.
 20    There are two types of nodes: functions and terminals.
 21
 22    (tree is not meant as a stand-alone -- parameters are supplied through the call from the Tree Creators)
 23
 24    Parameters
 25    ----------
 26    init_depth : (int, int)
 27        Min and max depths of initial random trees. The default is None.
 28        
 29    function_set : list
 30        List of functions used as internal nodes in the GP tree. The default is None.
 31        
 32    terminal_set : list
 33        List of terminals used in the GP-tree leaves. The default is None.
 34        
 35    erc_range : (float, float)
 36        Range of values for ephemeral random constant (ERC). The default is None.
 37    """
 38    def __init__(self,
 39                 fitness,
 40                 function_set=None,
 41                 terminal_set=None,
 42                 erc_range=None,
 43                 init_depth=(1, 2)):
 44        super().__init__(fitness)
 45        if function_set is None:
 46            function_set = [f_add, f_sub, f_mul, f_div]
 47
 48        if terminal_set is None:
 49            terminal_set = ['x', 'y', 'z', 0, 1, -1]
 50
 51        self.function_set = function_set
 52        self.terminal_set = terminal_set
 53        self.n_terminals = len(terminal_set)
 54        self.arity = dict([(func, arity(func)) for func in self.function_set])
 55        self.vars = [t for t in terminal_set if not isinstance(t, Number)]
 56        self.erc_range = erc_range
 57        self.n_functions = len(self.function_set)
 58        self.init_depth = init_depth
 59        self.tree = []
 60
 61    def size(self):
 62        """
 63        Compute size of tree.
 64
 65        Returns
 66        -------
 67        int
 68            tree size (= number of nodes).
 69        """
 70        return len(self.tree)
 71
 72    def add_tree(self, node):
 73        self.tree.append(node)
 74
 75    def empty_tree(self):
 76        self.tree = []
 77
 78    def _depth(self, pos, depth):
 79        """Recursively compute depth 
 80           (pos is a size-1 list so as to pass "by reference" on successive recursive calls)."""
 81
 82        node = self.tree[pos[0]]
 83
 84        depths = []
 85        if node in self.function_set:
 86            for i in range(self.arity[node]):
 87                pos[0] += 1
 88                depths.append(1 + self._depth(pos, depth + 1))
 89            return max(depths)
 90        else:
 91            return 0
 92
 93    def depth(self):
 94        """
 95        Compute depth of tree (maximal path length to a leaf).
 96
 97        Returns
 98        -------
 99        int
100            tree depth.
101        """
102
103        return self._depth([0], 0)
104
105    def random_function(self):
106        """select a random function"""
107        return self.function_set[randint(0, self.n_functions - 1)]
108
109    def random_terminal(self):
110        """Select a random terminal or create an ERC terminal"""
111        if self.erc_range is None:
112            node = self.terminal_set[randint(0, self.n_terminals - 1)]
113        else:
114            if random() > 0.5:
115                node = self.terminal_set[randint(0, self.n_terminals - 1)]
116            else:
117                node = round(uniform(*self.erc_range), 4)
118        return node
119
120    def _execute(self, pos, **kwargs):
121        """Recursively execute the tree by traversing it in a depth-first order 
122           (pos is a size-1 list so as to pass "by reference" on successive recursive calls)."""
123
124        node = self.tree[pos[0]]
125
126        if node in self.function_set:
127            arglist = []
128            for i in range(self.arity[node]):
129                pos[0] += 1
130                res = self._execute(pos, **kwargs)
131                arglist.append(res)
132            return node(*arglist)
133        else:  # terminal
134            if isinstance(node, Number):  # terminal is a constant
135                return node
136            else:  # terminal is a variable, return its value
137                return kwargs[node]
138
139    def execute(self, *args, **kwargs):
140        """
141        Execute the program (tree). 
142        Input is a numpy array or keyword arguments (but not both).
143
144        Parameters
145        ----------
146        args : arguments
147            A numpy array.
148        
149        kwargs : keyword arguments
150            Input to program, including every variable in the terminal set as a keyword argument.
151            For example, if `terminal_set=['x', 'y', 'z', 0, 1, -1]` 
152            then call `execute(x=..., y=..., z=...)`.
153
154        Returns
155        -------
156        object
157            Result of tree execution.
158        """
159
160        reshape = False
161        if args != ():  # numpy array -- convert to kwargs
162            try:
163                X = args[0]
164                kwargs = _generate_args(X)
165                reshape = True
166            except Exception:
167                raise ValueError(f'Bad argument to tree.execute, must be numpy array or kwargs: {args}')
168
169        kw = list(kwargs.keys())
170
171        bad_vars = [item for item in kw if item not in self.vars]
172        if len(bad_vars) > 0:
173            raise ValueError(f'tree.execute received variable arguments not in terminal set: {bad_vars}')
174
175        missing_vars = [item for item in self.vars if item not in kw]
176        if len(missing_vars) > 0:
177            raise ValueError(
178                f'Some variable terminals were not passed to tree.execute as keyword arguments: {missing_vars}')
179
180        res = self._execute([0], **kwargs)
181        if reshape and (isinstance(res, Number) or res.shape == np.shape(0)):
182            # sometimes a tree degenrates to a scalar value
183            res = np.full_like(X[:, 0], res)
184        return res
185
186    def random_subtree(self):
187        # select a random node index from this individual's tree
188        rnd_i = randint(0, self.size() - 1)
189        # find the subtree's end
190        end_i = self._find_subtree_end([rnd_i])
191        # now we have a random subtree from this individual
192        return self.tree[rnd_i:end_i + 1]
193
194    def _find_subtree_end(self, pos):
195        """find index of final node of subtree that starts at `pos` 
196          (pos is a size-1 list so as to pass "by reference" on successive recursive calls)."""
197
198        node = self.tree[pos[0]]
199
200        if node in self.function_set:
201            for i in range(self.arity[node]):
202                pos[0] += 1
203                self._find_subtree_end(pos)
204
205        return pos[0]
206
207    def replace_subtree(self, subtree):
208        """
209        Replace the subtree starting at `index` with `subtree`
210
211        Parameters
212        ----------
213        subtree - new subtree to replace the some existing subtree in this individual's tree
214
215        Returns
216        -------
217        None
218        """
219
220        index = randint(0, self.size() - 1)  # select a random node (index)
221        end_i = self._find_subtree_end([index])
222        if isinstance(self.tree[end_i], list):
223            print(self.tree[end_i], list)
224        left_part = self.tree[:index]
225        right_part = self.tree[(end_i + 1):]
226        self.tree = left_part + subtree + right_part
227
228    def _node_label(self, node):
229        """
230        return a string label for the node
231
232        Parameters
233        ----------
234        node - some node in the tree's function/terminal set
235
236        Returns
237        -------
238        node name - either a terminal (x0, x1,...) or a function (f_add, f_or, ...)
239        """
240        return node.__name__ if node in self.function_set else str(node)
241
242    def _show(self, prefix, pos):
243        """Recursively produce a simple textual printout of the tree 
244        (pos is a size-1 list so as to pass "by reference" on successive recursive calls)."""
245
246        node = self.tree[pos[0]]
247        if node in self.function_set:
248            print(f'{prefix}{self._node_label(node)}')
249            for i in range(self.arity[node]):
250                pos[0] += 1
251                self._show(prefix + "   ", pos)
252        else:  # terminal
253            print(f'{prefix}{self._node_label(node)}')
254
255    def show(self):
256        """
257        Print out a simple textual representation of the tree.
258
259        Returns
260        -------
261        None.
262        """
263        self._show("", [0])
264
265# end class tree
class Tree(eckity.individual.Individual):
 17class Tree(Individual):
 18    """
 19    A tree optimized for genetic programming operations.
 20    It is represented by a list of nodes in depth-first order.
 21    There are two types of nodes: functions and terminals.
 22
 23    (tree is not meant as a stand-alone -- parameters are supplied through the call from the Tree Creators)
 24
 25    Parameters
 26    ----------
 27    init_depth : (int, int)
 28        Min and max depths of initial random trees. The default is None.
 29        
 30    function_set : list
 31        List of functions used as internal nodes in the GP tree. The default is None.
 32        
 33    terminal_set : list
 34        List of terminals used in the GP-tree leaves. The default is None.
 35        
 36    erc_range : (float, float)
 37        Range of values for ephemeral random constant (ERC). The default is None.
 38    """
 39    def __init__(self,
 40                 fitness,
 41                 function_set=None,
 42                 terminal_set=None,
 43                 erc_range=None,
 44                 init_depth=(1, 2)):
 45        super().__init__(fitness)
 46        if function_set is None:
 47            function_set = [f_add, f_sub, f_mul, f_div]
 48
 49        if terminal_set is None:
 50            terminal_set = ['x', 'y', 'z', 0, 1, -1]
 51
 52        self.function_set = function_set
 53        self.terminal_set = terminal_set
 54        self.n_terminals = len(terminal_set)
 55        self.arity = dict([(func, arity(func)) for func in self.function_set])
 56        self.vars = [t for t in terminal_set if not isinstance(t, Number)]
 57        self.erc_range = erc_range
 58        self.n_functions = len(self.function_set)
 59        self.init_depth = init_depth
 60        self.tree = []
 61
 62    def size(self):
 63        """
 64        Compute size of tree.
 65
 66        Returns
 67        -------
 68        int
 69            tree size (= number of nodes).
 70        """
 71        return len(self.tree)
 72
 73    def add_tree(self, node):
 74        self.tree.append(node)
 75
 76    def empty_tree(self):
 77        self.tree = []
 78
 79    def _depth(self, pos, depth):
 80        """Recursively compute depth 
 81           (pos is a size-1 list so as to pass "by reference" on successive recursive calls)."""
 82
 83        node = self.tree[pos[0]]
 84
 85        depths = []
 86        if node in self.function_set:
 87            for i in range(self.arity[node]):
 88                pos[0] += 1
 89                depths.append(1 + self._depth(pos, depth + 1))
 90            return max(depths)
 91        else:
 92            return 0
 93
 94    def depth(self):
 95        """
 96        Compute depth of tree (maximal path length to a leaf).
 97
 98        Returns
 99        -------
100        int
101            tree depth.
102        """
103
104        return self._depth([0], 0)
105
106    def random_function(self):
107        """select a random function"""
108        return self.function_set[randint(0, self.n_functions - 1)]
109
110    def random_terminal(self):
111        """Select a random terminal or create an ERC terminal"""
112        if self.erc_range is None:
113            node = self.terminal_set[randint(0, self.n_terminals - 1)]
114        else:
115            if random() > 0.5:
116                node = self.terminal_set[randint(0, self.n_terminals - 1)]
117            else:
118                node = round(uniform(*self.erc_range), 4)
119        return node
120
121    def _execute(self, pos, **kwargs):
122        """Recursively execute the tree by traversing it in a depth-first order 
123           (pos is a size-1 list so as to pass "by reference" on successive recursive calls)."""
124
125        node = self.tree[pos[0]]
126
127        if node in self.function_set:
128            arglist = []
129            for i in range(self.arity[node]):
130                pos[0] += 1
131                res = self._execute(pos, **kwargs)
132                arglist.append(res)
133            return node(*arglist)
134        else:  # terminal
135            if isinstance(node, Number):  # terminal is a constant
136                return node
137            else:  # terminal is a variable, return its value
138                return kwargs[node]
139
140    def execute(self, *args, **kwargs):
141        """
142        Execute the program (tree). 
143        Input is a numpy array or keyword arguments (but not both).
144
145        Parameters
146        ----------
147        args : arguments
148            A numpy array.
149        
150        kwargs : keyword arguments
151            Input to program, including every variable in the terminal set as a keyword argument.
152            For example, if `terminal_set=['x', 'y', 'z', 0, 1, -1]` 
153            then call `execute(x=..., y=..., z=...)`.
154
155        Returns
156        -------
157        object
158            Result of tree execution.
159        """
160
161        reshape = False
162        if args != ():  # numpy array -- convert to kwargs
163            try:
164                X = args[0]
165                kwargs = _generate_args(X)
166                reshape = True
167            except Exception:
168                raise ValueError(f'Bad argument to tree.execute, must be numpy array or kwargs: {args}')
169
170        kw = list(kwargs.keys())
171
172        bad_vars = [item for item in kw if item not in self.vars]
173        if len(bad_vars) > 0:
174            raise ValueError(f'tree.execute received variable arguments not in terminal set: {bad_vars}')
175
176        missing_vars = [item for item in self.vars if item not in kw]
177        if len(missing_vars) > 0:
178            raise ValueError(
179                f'Some variable terminals were not passed to tree.execute as keyword arguments: {missing_vars}')
180
181        res = self._execute([0], **kwargs)
182        if reshape and (isinstance(res, Number) or res.shape == np.shape(0)):
183            # sometimes a tree degenrates to a scalar value
184            res = np.full_like(X[:, 0], res)
185        return res
186
187    def random_subtree(self):
188        # select a random node index from this individual's tree
189        rnd_i = randint(0, self.size() - 1)
190        # find the subtree's end
191        end_i = self._find_subtree_end([rnd_i])
192        # now we have a random subtree from this individual
193        return self.tree[rnd_i:end_i + 1]
194
195    def _find_subtree_end(self, pos):
196        """find index of final node of subtree that starts at `pos` 
197          (pos is a size-1 list so as to pass "by reference" on successive recursive calls)."""
198
199        node = self.tree[pos[0]]
200
201        if node in self.function_set:
202            for i in range(self.arity[node]):
203                pos[0] += 1
204                self._find_subtree_end(pos)
205
206        return pos[0]
207
208    def replace_subtree(self, subtree):
209        """
210        Replace the subtree starting at `index` with `subtree`
211
212        Parameters
213        ----------
214        subtree - new subtree to replace the some existing subtree in this individual's tree
215
216        Returns
217        -------
218        None
219        """
220
221        index = randint(0, self.size() - 1)  # select a random node (index)
222        end_i = self._find_subtree_end([index])
223        if isinstance(self.tree[end_i], list):
224            print(self.tree[end_i], list)
225        left_part = self.tree[:index]
226        right_part = self.tree[(end_i + 1):]
227        self.tree = left_part + subtree + right_part
228
229    def _node_label(self, node):
230        """
231        return a string label for the node
232
233        Parameters
234        ----------
235        node - some node in the tree's function/terminal set
236
237        Returns
238        -------
239        node name - either a terminal (x0, x1,...) or a function (f_add, f_or, ...)
240        """
241        return node.__name__ if node in self.function_set else str(node)
242
243    def _show(self, prefix, pos):
244        """Recursively produce a simple textual printout of the tree 
245        (pos is a size-1 list so as to pass "by reference" on successive recursive calls)."""
246
247        node = self.tree[pos[0]]
248        if node in self.function_set:
249            print(f'{prefix}{self._node_label(node)}')
250            for i in range(self.arity[node]):
251                pos[0] += 1
252                self._show(prefix + "   ", pos)
253        else:  # terminal
254            print(f'{prefix}{self._node_label(node)}')
255
256    def show(self):
257        """
258        Print out a simple textual representation of the tree.
259
260        Returns
261        -------
262        None.
263        """
264        self._show("", [0])

A tree optimized for genetic programming operations. It is represented by a list of nodes in depth-first order. There are two types of nodes: functions and terminals.

(tree is not meant as a stand-alone -- parameters are supplied through the call from the Tree Creators)

Parameters
  • init_depth ((int, int)): Min and max depths of initial random trees. The default is None.
  • function_set (list): List of functions used as internal nodes in the GP tree. The default is None.
  • terminal_set (list): List of terminals used in the GP-tree leaves. The default is None.
  • erc_range ((float, float)): Range of values for ephemeral random constant (ERC). The default is None.
Tree( fitness, function_set=None, terminal_set=None, erc_range=None, init_depth=(1, 2))
39    def __init__(self,
40                 fitness,
41                 function_set=None,
42                 terminal_set=None,
43                 erc_range=None,
44                 init_depth=(1, 2)):
45        super().__init__(fitness)
46        if function_set is None:
47            function_set = [f_add, f_sub, f_mul, f_div]
48
49        if terminal_set is None:
50            terminal_set = ['x', 'y', 'z', 0, 1, -1]
51
52        self.function_set = function_set
53        self.terminal_set = terminal_set
54        self.n_terminals = len(terminal_set)
55        self.arity = dict([(func, arity(func)) for func in self.function_set])
56        self.vars = [t for t in terminal_set if not isinstance(t, Number)]
57        self.erc_range = erc_range
58        self.n_functions = len(self.function_set)
59        self.init_depth = init_depth
60        self.tree = []
function_set
terminal_set
n_terminals
arity
vars
erc_range
n_functions
init_depth
tree
def size(self):
62    def size(self):
63        """
64        Compute size of tree.
65
66        Returns
67        -------
68        int
69            tree size (= number of nodes).
70        """
71        return len(self.tree)

Compute size of tree.

Returns
  • int: tree size (= number of nodes).
def add_tree(self, node):
73    def add_tree(self, node):
74        self.tree.append(node)
def empty_tree(self):
76    def empty_tree(self):
77        self.tree = []
def depth(self):
 94    def depth(self):
 95        """
 96        Compute depth of tree (maximal path length to a leaf).
 97
 98        Returns
 99        -------
100        int
101            tree depth.
102        """
103
104        return self._depth([0], 0)

Compute depth of tree (maximal path length to a leaf).

Returns
  • int: tree depth.
def random_function(self):
106    def random_function(self):
107        """select a random function"""
108        return self.function_set[randint(0, self.n_functions - 1)]

select a random function

def random_terminal(self):
110    def random_terminal(self):
111        """Select a random terminal or create an ERC terminal"""
112        if self.erc_range is None:
113            node = self.terminal_set[randint(0, self.n_terminals - 1)]
114        else:
115            if random() > 0.5:
116                node = self.terminal_set[randint(0, self.n_terminals - 1)]
117            else:
118                node = round(uniform(*self.erc_range), 4)
119        return node

Select a random terminal or create an ERC terminal

def execute(self, *args, **kwargs):
140    def execute(self, *args, **kwargs):
141        """
142        Execute the program (tree). 
143        Input is a numpy array or keyword arguments (but not both).
144
145        Parameters
146        ----------
147        args : arguments
148            A numpy array.
149        
150        kwargs : keyword arguments
151            Input to program, including every variable in the terminal set as a keyword argument.
152            For example, if `terminal_set=['x', 'y', 'z', 0, 1, -1]` 
153            then call `execute(x=..., y=..., z=...)`.
154
155        Returns
156        -------
157        object
158            Result of tree execution.
159        """
160
161        reshape = False
162        if args != ():  # numpy array -- convert to kwargs
163            try:
164                X = args[0]
165                kwargs = _generate_args(X)
166                reshape = True
167            except Exception:
168                raise ValueError(f'Bad argument to tree.execute, must be numpy array or kwargs: {args}')
169
170        kw = list(kwargs.keys())
171
172        bad_vars = [item for item in kw if item not in self.vars]
173        if len(bad_vars) > 0:
174            raise ValueError(f'tree.execute received variable arguments not in terminal set: {bad_vars}')
175
176        missing_vars = [item for item in self.vars if item not in kw]
177        if len(missing_vars) > 0:
178            raise ValueError(
179                f'Some variable terminals were not passed to tree.execute as keyword arguments: {missing_vars}')
180
181        res = self._execute([0], **kwargs)
182        if reshape and (isinstance(res, Number) or res.shape == np.shape(0)):
183            # sometimes a tree degenrates to a scalar value
184            res = np.full_like(X[:, 0], res)
185        return res

Execute the program (tree). Input is a numpy array or keyword arguments (but not both).

Parameters
  • args (arguments): A numpy array.
  • kwargs (keyword arguments): Input to program, including every variable in the terminal set as a keyword argument. For example, if terminal_set=['x', 'y', 'z', 0, 1, -1] then call execute(x=..., y=..., z=...).
Returns
  • object: Result of tree execution.
def random_subtree(self):
187    def random_subtree(self):
188        # select a random node index from this individual's tree
189        rnd_i = randint(0, self.size() - 1)
190        # find the subtree's end
191        end_i = self._find_subtree_end([rnd_i])
192        # now we have a random subtree from this individual
193        return self.tree[rnd_i:end_i + 1]
def replace_subtree(self, subtree):
208    def replace_subtree(self, subtree):
209        """
210        Replace the subtree starting at `index` with `subtree`
211
212        Parameters
213        ----------
214        subtree - new subtree to replace the some existing subtree in this individual's tree
215
216        Returns
217        -------
218        None
219        """
220
221        index = randint(0, self.size() - 1)  # select a random node (index)
222        end_i = self._find_subtree_end([index])
223        if isinstance(self.tree[end_i], list):
224            print(self.tree[end_i], list)
225        left_part = self.tree[:index]
226        right_part = self.tree[(end_i + 1):]
227        self.tree = left_part + subtree + right_part

Replace the subtree starting at index with subtree

Parameters
  • subtree - new subtree to replace the some existing subtree in this individual's tree
Returns
  • None
def show(self):
256    def show(self):
257        """
258        Print out a simple textual representation of the tree.
259
260        Returns
261        -------
262        None.
263        """
264        self._show("", [0])

Print out a simple textual representation of the tree.

Returns
  • None.