Source code for cartesian_genetics_base.cartesian_genome_func

"""
``cartesian_genome_func`` is module with cartesian genome functions representation. Currently consists of pure python
3.x non-optimized ``CartesianGenomeFunc``.




Copyright (C) 2021 Evgenii Tsatsorin eugtsa@gmail.com

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""

import math
import random
from inspect import signature


[docs]class CartesianGenomeFunc: """``CartesianGenomeFunc`` class is simple and naive CGP function implementation (https://en.wikipedia.org/wiki/Cartesian_genetic_programming). It is still not optimized, goes front to back, propagate through all nodes to calculate result Args: n_inputs (int): number on inputs n_outputs (int): number of outputs depth (int): depth of genome func representation n_rows (int): number of functions on each layer of depth recurse_depth (int): depth of previous layers allowed to transmit inputs to each next level arity (int): arity of basis functions, if not set then would be determined automatically on given basis seed (int): random seed for random operations (init_random_genome and such) basis_funcs (list): list of callable, basis functions for genome func representations """ def __init__(self, n_inputs=None, n_outputs=None, depth=None, n_rows=None, basis_funcs=None, recurse_depth=1, arity=None, seed=None): """CGP function representation. Creates cartesian genome function. This function can calculate expressions with given genome and basis. Args: n_inputs (int): number on inputs n_outputs (int): number of outputs depth (int): depth of genome func representation n_rows (int): number of functions on each layer of depth recurse_depth (int): depth of previous layers allowed to transmit inputs to each next level arity (int): arity of basis functions, if not set then would be determined automatically on given basis seed (int): random seed for random operations (init_random_genome and such) basis_funcs (list): list of callable, basis functions for genome func representations Returns: CartesianGenomeFunc: constructed CG function representation """ self._basis_funcs = basis_funcs self._arity = arity if arity is None: self._count_and_set_max_arity_on_basis(basis_funcs) self._n_inputs = n_inputs self._n_outputs = n_outputs self._depth = depth self._recurse_depth = recurse_depth self._layers_calls = list() self._n_rows = n_rows self._layer_funcs = list() self._init_layers() self.seed = seed if seed is not None: random.seed(seed) self._genome = ([1, ] * self._n_rows * (self._arity + 1)) * self._depth + [1, ] * self._n_outputs def _count_and_set_max_arity_on_basis(self, basis): max_arity = 0 for func in basis: func_arity = len(signature(func).parameters) if func_arity > max_arity: max_arity = func_arity self._arity = max_arity
[docs] def set_basis(self,new_basis): """Set basis functions to this genome function Args: new_basis (list): list of callables for use as basis functions Returns: None: nothing to return """ self._basis_funcs = new_basis self._count_and_set_max_arity_on_basis() self._recreate_layer_funcs()
def _init_layers(self): self._layers_calls.append([False, ] * self._n_inputs) for d in range(self._depth): self._layers_calls.append([False, ] * self._n_rows) self._layers_calls.append([False, ] * self._n_outputs)
[docs] def get_genome(self): """Get current genome representation Returns: list: list of floats with current genome """ return self._genome
[docs] def get_int_genome(self): """Get current genome representation as ``int`` type, could be used to well-build optimisation. Todo in 0.0.2 version Returns: list: list of ints with current genome """
[docs] def set_genome(self,new_genome): """Validate and set genome using current basis Args: new_genome: list of floats Returns: None: inplace operation, returns None """ assert len(new_genome) == self._n_rows*(self._arity+1)*self._depth+self._n_outputs assert all([v>=0.0 and v<=1.0 for v in new_genome]) self._genome = new_genome self._recreate_layer_funcs()
def _recreate_layer_funcs(self): if len(self._layer_funcs)!=0: self._layer_funcs.clear() for l in range(self._depth): offset = l*(self._n_rows*(self._arity+1)) self._layer_funcs.append(list()) for row_num in range(self._n_rows): func_index_from = row_num * (self._arity + 1) func_index_to = (row_num+1) * (self._arity + 1) func_code,*inputs_codes = self._genome[offset+func_index_from: offset+func_index_to] decoded_function = self._get_function_from_basis(func_code) # appending func to last layer self._layer_funcs[-1].append((decoded_function,inputs_codes)) def _get_function_from_basis(self,func_num): func_index = math.floor(func_num * len(self._basis_funcs)) return self._basis_funcs[func_index]
[docs] def call(self, input_vals): """Call genome function with input vals Args: input_vals (list): list of input arguments (arguments type depends on basis functions) Returns: list: output values from output layer """ self._make_top_down_propagation(input_vals) return list(self._layers_calls[-1])
def _make_top_down_propagation(self,inputs): for i,v in enumerate(inputs): self._layers_calls[0][i] = v for l in range(self._depth): self._propagate_calls(to_layer=l) self._propagate_outputs() return self._layers_calls[-1] def _propagate_outputs(self): last_inputs = self._get_inputs_for_layer(self._depth) encoded_outs = self.get_genome()[-self._n_outputs:] out_value = self._decode_and_get_inputs(last_inputs,encoded_outs) for i,v in enumerate(out_value): self._layers_calls[-1][i] = v def _propagate_calls(self,to_layer=None): layer_functions_with_encoded_inputs = self._layer_funcs[to_layer] layer_total_inputs = self._get_inputs_for_layer(to_layer) for i,(layer_func,encoded_inputs) in enumerate(layer_functions_with_encoded_inputs): func_input = self._decode_and_get_inputs(layer_total_inputs,encoded_inputs) func_arity = len(signature(layer_func).parameters) func_result = layer_func(*(func_input[:func_arity])) self._layers_calls[to_layer+1][i] = func_result def _decode_and_get_inputs(self,total_inputs,encoded_inputs): return tuple(total_inputs[math.floor(i*len(total_inputs))] for i in encoded_inputs) def _get_inputs_for_layer(self, layer_num): inputs = list() for depth in range(0, self._recurse_depth): if layer_num-depth>=0: inputs.extend(self._layers_calls[layer_num-depth]) return inputs
[docs] def init_random_genome(self): """Inits random genome with uniform distribution Returns: None: inits random genome inplace, doesn't return anything """ self.set_genome([random.random() for _ in self._genome])