# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/01_evolvers.ipynb (unless otherwise specified).

__all__ = ['BaseEvolver', 'EvolverWrapper', 'SigmoidEvolver', 'StandardEvolver', 'DynamicEvolver',
           'check_hash_file_exists', 'evolve_from_properties_file']

# Cell
from jproperties import Properties
import random
import numpy as np
import math
import time
import enum
import networkx
import os
import socket
import hashlib
from networkx.drawing.nx_agraph import graphviz_layout
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib import style
from deap import algorithms
from abc import ABC, abstractmethod
from deap import base
from deap import creator
from deap import tools
from deap.tools import History
#from scoop import futures
import multiprocessing
from datetime import datetime

from .structure import StructureDefinition
from pct.putils import sigmoid
from .configs import ProportionalConfiguration
from .configs import BaseConfiguration
from .configs import DynamicConfiguration
from .configs import DynamicConfigurationStructure
from .configs import ConfigurationStructure
from pct.putils import stringIntListToListOfInts
from pct.putils import stringFloatListToListOfFloats
from pct.putils import stringListToListOfStrings
from pct.putils import listNumsToString
from pct.putils import get_drive
from pct.architectures import ProportionalArchitecture
from pct.architectures import DynamicArchitecture
from pct.structure  import ArchitectureStructure
from pct.nodes import ControlUnitIndices
from pct.architectures import LevelKey
from pct.environments import VelocityModel
from pct.environments import OpenAIGym
from .structure import BinaryOnes
from pct.errors import RootSumSquaredError
from pct.errors import TotalError
from pct.errors import BaseErrorCollector
from pct.putils import get_drive
from pct.environments import EnvironmentFactory
from pct.architectures import load_properties

# Cell
class BaseEvolver(ABC):
    "Base class of an evolver. This class is not used direclty by developers, but defines the interface common to all."
    def __init__(self):
        self.mutate_structure_sum=0
        self.mutate_sum=0

    def set_toolbox(self, toolbox):
        self.toolbox=toolbox

    def ensure_non_zero(self, item, index):
        if np.sum(item[index])==0:
            length = len(item[index])
            ind = random.randint(0, length-1)
            item[index][ind]=1

    def reset_mutate_structure_sum(self):
        sum = self.mutate_structure_sum
        self.mutate_structure_sum = 0
        return sum

    def reset_mutate_sum(self):
        sum = self.mutate_sum
        self.mutate_sum=0
        return sum

    def set_gen(self, gen):
        self.gen=gen

    def get_level_key(self, level, levels):
        key = LevelKey.N
        if level == 0:
            key = LevelKey.ZERO
        if level == levels-1:
            key = LevelKey.TOP

        return key

    @abstractmethod
    def create(self, cls):
        return cls(new_individial)

    @abstractmethod
    def evaluate(self, individual):
        return score,

    @abstractmethod
    def mate(self, ind1, ind2):
        return ind1, ind2

    @abstractmethod
    def mutate(self, ind, indpb=0.05):
        return ind,

    @abstractmethod
    def create_plot(self):
        return 0

    @abstractmethod
    def animate(self, epoch):
        pass

# Cell
class EvolverWrapper():

    def __init__(self, evolver, pop_size=25, p_crossover = 0.9, p_mutation = 0.1, display_env=False,
                 select={'selection_type':'tournament', 'tournsize':None}, parallel=False,
                 save_arch_gen=False, save_arch_all=False, **cargs):
        self.timing_sum=0
        self.save_arch_gen=save_arch_gen
        self.save_arch_all=save_arch_all
        self.display_env=display_env
        self.evolver=evolver
        self.pop_size=pop_size
        self.p_crossover = p_crossover   # probability for crossover
        self.p_mutation = p_mutation   # probability for mutating an individual
        if hasattr(creator, 'FitnessMin'):
            del creator.FitnessMin
            del creator.Individual
        creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
        creator.create("Individual", list, fitness=creator.FitnessMin)
        # Structure initializers
        self.toolbox = base.Toolbox()

        if parallel:
            pool = multiprocessing.Pool()
            self.toolbox.register("map", pool.map)
            #self.toolbox.register("map", futures.map)

        self.toolbox.register("individual", self.create(), creator.Individual)
        self.toolbox.register("population", tools.initRepeat, list, self.toolbox.individual)

        self.toolbox.register("evaluate", self.evaluate())
        self.toolbox.register("mate", self.mate())
        self.toolbox.register("mutate", self.mutate())

        if select['selection_type'] == 'tournament':
            if 'tournsize' in select.keys():
                tournsize=select['tournsize']
            else:
                tournsize=None
            if tournsize==None:
                tournsize=(int)(pop_size/4)
            self.toolbox.register("select", tools.selTournament, tournsize=tournsize)

        if select['selection_type'] ==  'best':
            self.toolbox.register("select", tools.selBest)

        genealogy = History()
        self.toolbox.decorate("mate", genealogy.decorator)
        self.toolbox.decorate("mutate", genealogy.decorator)
        self.hof = tools.HallOfFame(1)

        #self.pop = self.toolbox.population(n=pop_size)
        #history.update(self.pop)

        self.genealogy=genealogy

        self.stats = tools.Statistics(lambda ind: ind.fitness.values)
        self.stats.register("mean", np.mean)
        self.stats.register("min", np.min)
        self.stats.register("max", np.max)
        self.stats.register("std", np.std)
        self.evolver.set_toolbox(self.toolbox)
        self.stats_history={}
        self.stats_history['min'] = []

    def create(self):
        return self.evolver.create

    def evaluate(self):
        return self.evolver.evaluate

    def mate(self):
        return self.evolver.mate

    def mutate(self):
        return self.evolver.mutate

    def create_population(self):
        self.pop = self.toolbox.population(n=self.pop_size)
        return self.pop

    def collect_stats(self, gen, index, logbook, timeperpop, verbose, log):
        min = logbook[index]['min']
        mean = logbook[index]['mean']
        max = logbook[index]['max']
        #std = logbook[index]['std']
        log_string=''
        self.stats_history['min'].append(min)

        if index > 0:
            mut = 100*self.evolver.reset_mutate_sum()/self.pop_size
            muts = 100*self.evolver.reset_mutate_structure_sum()/self.pop_size

        if verbose>0 or log:
            if index > 0:
                logs = f'{gen:4} {len(self.pop):4} {min:8.3f} {mean:10.3f} {max:10.3f}  {mut:3.0f}% {muts:3.0f}%   {timeperpop:4.3f}'
            else:
                logs = f'{gen:4} {len(self.pop):4} {min:8.3f} {mean:10.3f} {max:10.3f} \n'

        if log:
            log_string = ''.join(('# ', logs))
        if verbose>0:
            print(logs, end = '')

        return log_string

    def run(self, gens=25, verbose=False, deap_verbose=False, log=False):
        log_string = ''

        if self.save_arch_all:
            self.evolver.set_save_arch_all(self.save_arch_all)
            self.evolver.set_gen(0)

        self.pop = self.toolbox.population(n=self.pop_size)
        self.genealogy.update(self.pop)

        if verbose>0:
            logs = 'gen   pop   min       mean      max        mut  muts  timing'
            print(logs)
        if log:
            logs = 'gen   pop   min       mean      max        mut  muts  timing'
            log_string = ''.join((log_string, '# ', logs, '\n'))

        self.best_of_gens=[]
        top_ind = tools.selBest(self.pop, k=1)[0]
        self.best_of_gens.append(top_ind)

        for gen in range(1, gens+1, 1):
            self.evolver.set_gen(gen)
            tic = time.perf_counter()
            self.pop, logbook = algorithms.eaSimple(self.pop, self.toolbox, cxpb=self.p_crossover,
                    mutpb=self.p_mutation,  ngen=1, halloffame=self.hof, stats=self.stats, verbose=deap_verbose)
            toc = time.perf_counter()
            timeperpop = (toc-tic)/self.pop_size
            self.timing_sum += timeperpop
            self.evolver.member=0
            if gen == 1:
                log_stats = self.collect_stats( 0, 0, logbook, timeperpop, verbose, log)
                if log:
                    log_string = ''.join((log_string, log_stats))

            log_stats = self.collect_stats( gen, 1, logbook, timeperpop, verbose, log)

            if verbose>2:
                print('pop ***')
                for pop in self.pop:
                    print(pop)

            top_ind = tools.selBest(self.pop, k=1)[0]
            if verbose>1:
                print ( f'[{top_ind}]')
            else:
                if verbose>0:
                    print()
                if log:
                    log_string = ''.join((log_string, log_stats, '\n'))

            self.best_of_gens.append(top_ind)
            if self.display_env:
                if hasattr(self.evolver, 'env'):
                    if isinstance(self.evolver.env, OpenAIGym):
                        self.evolver.env.set_render(True)
                        if self.save_arch_gen:
                            self.evolver.fig_file = f'fig{gen:03}.png'
                        if gen == 1:
                            print('Displaying gen 0')
                            self.evolver.env.set_render(True)
                            self.evolver.evaluate(self.best_of_gens[0])
                            print('>> Press enter to continue:')
                            input()
                            print('Displaying gen 0')
                            self.evolver.env.set_render(True)
                            self.evolver.evaluate(self.best_of_gens[0])
                            self.evolver.env.set_render(True)
                        print(f'Displaying gen {gen}')
                        self.evolver.evaluate(top_ind)
                        self.evolver.fig_file = None
                        self.evolver.env.set_render(False)
                        print(top_ind)

        return self.timing_sum/gens, log_string

    def draw_tree(self, node_size=500, with_labels=True):
        graph = networkx.DiGraph(self.genealogy.genealogy_tree)
        graph = graph.reverse()     # Make the graph top-down
        colors = [self.toolbox.evaluate(self.genealogy.genealogy_history[i])[0] for i in graph]
        pos=graphviz_layout(graph, prog="dot")
        figsize=(14,20)
        plt.figure(figsize=figsize)
        networkx.draw(graph, pos=pos, node_color=colors, node_size=node_size,  with_labels=with_labels)

    def best(self):
        return self.hof[0]

    def best_score(self):
        return self.hof[0].fitness.values[0]

    def get_best_of_gens(self):
        return self.best_of_gens

    def create_stats_plot(self, data):
        first_key = list(data.keys())[0]
        self.x = np.linspace(0, len(data[first_key])-1, len(data[first_key]))
        style.use('fivethirtyeight')
        self.fig = plt.figure(figsize=(10,5))
        self.ax1 = self.fig.add_subplot(1,1,1)
        plt.title('Stats')
        self.ax1.grid(True)

        for datum in data:
            self.ax1.plot(self.x, data[datum],  c='r')

        return self.fig


# Cell
class SigmoidEvolver(BaseEvolver):
    "A class to evolve a sigmoid function defined by two parameters the range and scale (slope)."
    'Parameters:'
    'range_limit - the initial limit of the range for an individual'
    'scale_limit - the initial limit of the scale for an individual'
    'alpha - for crossover, the extent to which the new values can be drawn on both side of the parents’ attributes'
    'mu - for mutation, mean for the gaussian mutation'
    'sigma - standard deviation for the gaussian mutation'
    'indpb - probability for each attribute to be mutated'
    'evaluate_factor - factor at which goal target is evaulated'
    'evaluate_divisor - division of goal at which half the goal is evaluated'

    def __init__(self, goal=7, range_limit=20, scale_limit=5, alpha=0.5, mu=0.1, sigma=0.25, indpb=0.2,
                 evaluate_factor=5, evaluate_divisor=5,  **cargs):
        super().__init__()
        self.goal=goal
        self.range_limit=range_limit
        self.scale_limit=scale_limit
        self.alpha=alpha
        self.mu=mu
        self.sigma=sigma
        self.evaluate_factor=evaluate_factor
        self.evaluate_divisor=evaluate_divisor
        self.indpb=indpb

    def create(self, cls):
        new_individial = []
        new_individial.append(random.uniform(0, self.range_limit))
        new_individial.append(random.uniform(0, self.scale_limit))

        return cls(new_individial)

    def evaluate(self, individual):

        shift = 0
        upper = 0
        lower = 0
        zero = 0

        goal = self.goal * self.evaluate_factor
        upper =shift + sigmoid(goal, individual[0], individual[1])
        lower = shift + sigmoid(self.goal/self.evaluate_divisor, individual[0], individual[1])
        zero = sigmoid(0, individual[0], individual[1])

        score = abs(self.goal - upper) + abs(self.goal/2 - lower) + shift - zero
        return score,

    def mate(self, ind1, ind2):
        ind1, ind2 = tools.cxBlend(ind1, ind2, self.alpha)
        if ind1[0]<0 or ind2[0]<0 :
            if ind1[0]<0 :
               ind1[0]=abs(ind1[0])
            if ind2[0]<0 :
               ind2[0]=abs(ind2[0])

        if ind1[1]<0 or ind2[1]<0 :
            if ind1[1]<0 :
               ind1[1]=abs(ind1[1])
            if ind2[1]<0 :
               ind2[1]=abs(ind2[1])

        return ind1, ind2

    def mutate(self, ind):

        mutant = ind
        #print('indpb',self.indpb)
        mutant, = tools.mutGaussian(ind, mu=self.mu, sigma=self.sigma, indpb=self.indpb)
        return mutant,


    def create_plot(self, best_of_gens):
        self.best_of_gens=best_of_gens
        self.x = np.linspace(-50, 50, 101)
        self.frames = len(best_of_gens)
        top_ind = self.best_of_gens[-1]
        self.ytarget = sigmoid(self.x, top_ind[0], top_ind[1])
        style.use('fivethirtyeight')
        self.fig = plt.figure(figsize=(10,5))
        self.ax1 = self.fig.add_subplot(1,1,1)
        plt.title('Target')
        self.ax1.grid(True)
        self.ax1.plot(self.x, self.ytarget,  c='r')
        return self.fig

    def animate(self, epoch):
        if epoch <= self.frames:
            top_ind = self.best_of_gens[epoch]
            y = sigmoid(self.x, top_ind[0], top_ind[1])
            self.ax1.clear()
            self.ax1.grid(True)
            self.ax1.plot(self.x, self.ytarget,  c='r')
            self.ax1.plot(self.x, y,  c='b', linewidth=2)

# Cell
class StandardEvolver(BaseEvolver):
    "A class to evolve PCT hierarchies."
    'Parameters:'
    'error_collector - the class for collecting the errors'
    'env - the environment simulation'
    'runs - the number of generations'
    'alpha - for crossover, the extent to which the new values can be drawn on both side of the parents’ attributes'
    'mu - for mutation, mean for the gaussian mutation'
    'sigma - standard deviation for the gaussian mutation'
    'inputs - the list of the indices of the perceptions from the environment'
    'num_actions - the number of actions in the environment'
    'references - the list of reference values'
    'levels_limit - the maximum number of levels in initial population individuals'
    'columns_limit - the maximum number of comumns in initial population individuals'
    'lower_float - the lower limit of float values for weights'
    'upper_float - the upper limit of float values for weights'
    'structurepb - the probability of a change in structure'
    'indpb - probability for each attribute to be mutated'

    def __init__(self, env=None, inputs=None, num_actions=1, references=[1], levels_limit=3,
                 columns_limit=3, alpha=0.5, mu=0.1, sigma=0.25, indpb=0.2, runs=100,
                 lower_float=-100, upper_float=100, error_response_type=None, error_collector_type=None,
                 error_limit=None, structurepb=0.1, debug=0, seed=None, hpct_verbose=False,
                 nevals=1, file= None, **cargs):
        super().__init__()
        if inputs==None:
            raise Exception('Inputs must be defined')
        self.nevals = nevals
        self.hpct_verbose=hpct_verbose
        self.error_collector_type=error_collector_type
        self.error_response_type=error_response_type
        self.error_limit=error_limit
        if error_collector_type == None:
            self.error_collector_type='TotalError'
        if error_response_type == None:
            self.error_response_type='RootSumSquaredError'
        if error_limit == None:
            self.error_limit=500
        self.env=env
        self.runs=runs
        self.alpha=alpha
        self.mu=mu
        self.sigma=sigma
        self.inputs=inputs
        self.num_actions=num_actions
        self.references=references
        self.levels_limit=levels_limit
        self.columns_limit=columns_limit
        self.lower_float=lower_float
        self.upper_float=upper_float
        self.structurepb=structurepb
        self.debug=debug
        self.indpb=indpb
        self.seed=seed
        random.seed(seed)
        if self.debug>0:
            print('Initial population')
        self.fig_file=file
        self.member=0
        self.save_arch_all=False


    def set_save_arch_all(self, save_arch_all):
        self.save_arch_all = save_arch_all

    def create(self, cls):
        grid=[]
        levels = random.randint(0, self.levels_limit)
        #print(levels)
        for level in range(levels-1):
            columns = random.randint(1, self.columns_limit)
            grid.append(columns)

        grid.append(len(self.references))

        #print(grid)
        new_individual = ProportionalConfiguration(num_inputs=len(self.inputs), num_actions=self.num_actions,
            grid=grid, references=self.references, lower_float=self.lower_float, upper_float=self.upper_float)
        new_individual()
        if self.debug>0:
            print(new_individual.to_raw())
        return cls(new_individual.to_raw())

    def evaluate(self, individual):

        if self.save_arch_all:
            self.fig_file = f'arch-{self.gen:04}-{self.member:04}.png'

        config = BaseConfiguration.from_raw(individual)

        score = 0

        for i in range(self.nevals):
            error_collector = BaseErrorCollector.collector(self.error_response_type, self.error_collector_type, self.error_limit)
            if self.seed != None:
                self.env.set_seed(self.seed)
            self.env.reset()
            pa = ProportionalArchitecture(config=config, env=self.env, input_indexes=self.inputs, error_collector=error_collector)
            pa()
            hpct = pa.get_hierarchy()
            if self.fig_file != None:
                hpct.draw(file=self.fig_file)

            hpct.run(steps=self.runs, verbose=self.hpct_verbose)
            score += hpct.get_error_collector().error()

        if self.debug>1:
            print(f'{self.gen:03} {self.member:03} score {score:5.3f}', config)

        self.member+=1

        return score,


    def datatype(self, data):
        print(data)
        type = None
        if isinstance(data[0], int):
            return "int"

        return type

    def process_crossover(self, child1item, child2item, index):
        if isinstance(child1item[index][0], int):
            child1item[index], child2item[index] = tools.cxUniform(child1item[index], child2item[index], 0.5)
            self.ensure_non_zero(child1item, index)
            self.ensure_non_zero(child2item, index)

        if isinstance(child1item[index][0], float):
            child1item[index], child2item[index] = tools.cxBlend(child1item[index], child2item[index], self.alpha)


    def mate(self, ind1, ind2):
        child1, child2 = [self.toolbox.clone(ind) for ind in (ind1, ind2)]

        child2levellen = len(child2)
        for level in range(len(child1)):
            if level > child2levellen-1:
                break
            for item in range(3):#len(child1[level])):
                #if item ==3: print(child1[level][item])
                #print('level', level, 'item',item)
                if isinstance(child1[level][item][0], list):
                    child2itemlen = len(child2[level][item])
                    for index in range(len(child1[level][item])):
                        #print(index, child2itemlen)
                        if index > child2itemlen-1:
                            break
                        self.process_crossover(child1[level][item], child2[level][item], index)
                else:
                    self.process_crossover(child1[level], child2[level], item)

        if self.debug>2:
            print('mate b4',ind1)
            print('mate b4',ind2)
            print('mate b5',child1)
            print('mate b5',child2)

        return child1, child2

    def mutate(self, ind):
        mutated = 0
        mutated_structure = 0
        mutant = self.toolbox.clone(ind)
        if self.debug>2:
            print('mut b4',mutant)
        for level in range(len(mutant)):
            for item in range(3):
                if isinstance(mutant[level][item][0], list):
                    for index in range(len(mutant[level][item])):
                        mutated += self.process_mutant(mutant[level][item], index)
                else:
                    mutated += self.process_mutant(mutant[level], item)

        if random.random() < self.structurepb:
            mutant, mutated_structure = self.mutate_structure(mutant)

        if mutated>0 or mutated_structure>0:
            self.mutate_sum += 1

        if mutated_structure>0:
            self.mutate_structure_sum += 1

        if self.debug>2:
            print('mut b5',mutant)
        return mutant,

    def mutate_structure(self, mutant):
        mutated_structure = 0
        raw=mutant
        #print('mutate_structure')
        #print(raw)
        orig_config=BaseConfiguration.from_raw(raw)
        cs = ConfigurationStructure()

        struct = cs.from_raw(raw)
        #print()
        #print(struct)

        choice = random.randint(1,4)
        #print('choice',choice)
        levels = len(struct['grid'])
        if choice == 1 :
            upper_levels_limit = levels-2
            if upper_levels_limit >= 0:
                level = random.randint(0, levels-2)
                num_nodes = random.randint(1, self.columns_limit)
                #print(level, num_nodes)
                struct, mutated_structure = cs.add_nodes(level=level, num_nodes=num_nodes)

        if choice == 2 :
            num_columns = random.randint(1, self.columns_limit)
            struct, mutated_structure = cs.add_level(num_columns=num_columns)

        if choice == 3 :
            struct, mutated_structure = cs.remove_level()

        if choice == 4 :
            upper_levels_limit = levels-2
            if upper_levels_limit >= 0:
                remove_level = random.randint(0, upper_levels_limit)
                upper_node_limit = struct['grid'][remove_level]-1
                if upper_node_limit > 1:
                    remove_nodes = random.randint(1, upper_node_limit)
                    #print(remove_level, remove_nodes)
                    struct, mutated_structure = cs.remove_nodes(level=remove_level, num_nodes=remove_nodes)

        #print(struct)


        pc=ProportionalConfiguration(lower_float=self.lower_float,upper_float=self.upper_float, **struct)
        config=pc()
        #print(config)


        if choice == 1 :
            BaseConfiguration.copy_data(orig_config, config)
        if choice == 2 :
            BaseConfiguration.copy_data_from_smaller(orig_config, config)
        if choice == 3 :
            if len(orig_config) != len(config):
                #print(orig_config)
                #print(config)
                BaseConfiguration.copy_data_from_larger(orig_config, config)
            else:
                config=orig_config
        if choice == 4 :
            BaseConfiguration.copy_data(orig_config, config)

        #print(config)

        mutant = creator.Individual(BaseConfiguration.dict_to_raw(config))

        return mutant, mutated_structure

    def process_mutant(self, mutant, index):
        mutated = 0
        if isinstance(mutant[index][0], int) and len(mutant[index])>1:
            orig = self.toolbox.clone(mutant[index])
            mutant[index], = tools.mutFlipBit(mutant[index], self.indpb)
            if np.sum(mutant[index]) == 0:
                pos = random.randint(0, len(mutant[index])-1)
                #print('pos', pos)
                #print(mutant[index])
                mutant[index][pos]=1
            if orig != mutant[index]:
                mutated = 1

        if isinstance(mutant[index][0], float):
            orig = self.toolbox.clone(mutant[index])
            mutant[index], = tools.mutGaussian(mutant[index],  self.mu, self.sigma, self.indpb)
            if orig != mutant[index]:
                mutated = 1

        return mutated

    """
    def create_stats_plot(self, data):
        first_key = list(data.keys())[0]
        self.x = np.linspace(0, len(data[first_key])-1, len(data[first_key]))
        style.use('fivethirtyeight')
        self.fig = plt.figure(figsize=(10,5))
        self.ax1 = self.fig.add_subplot(1,1,1)
        plt.title('Stats')
        self.ax1.grid(True)

        for datum in data:
            self.ax1.plot(self.x, data[datum],  c='r')

        return self.fig
    """

    def create_plot(self, best_of_gens):
        self.best_of_gens=best_of_gens
        self.x = np.linspace(0, self.runs-1, self.runs)
        self.frames = len(best_of_gens)
        top_ind = self.best_of_gens[-1]
        config = BaseConfiguration.from_raw(top_ind)
        if self.debug == 1:
            print(config)
        self.env.reset()
        pa = ProportionalArchitecture(config=config, env=self.env, input_indexes=self.inputs, history=True)
        pa()
        hpct = pa.get_hierarchy()
        hpct.run(steps=self.runs)
        self.ytarget =  hpct.get_node(0, 0).history.data['percoll']['PL0C0ws']
        style.use('fivethirtyeight')
        self.fig = plt.figure(figsize=(10,5))
        self.ax1 = self.fig.add_subplot(1,1,1)
        plt.title('Target')
        self.ax1.grid(True)
        self.ax1.plot(self.x, self.ytarget,  c='r')
        return self.fig

    def animate(self, epoch):
        if epoch <= self.frames:
            top_ind = self.best_of_gens[epoch]
            config = BaseConfiguration.from_raw(top_ind)
            if self.debug == 1:
                print(config)
            self.env.reset()
            pa = ProportionalArchitecture(config=config, env=self.env, input_indexes=self.inputs, history=True)
            pa()
            hpct = pa.get_hierarchy()
            hpct.run(steps=self.runs)
            y =  hpct.get_node(0, 0).history.data['percoll']['PL0C0ws']
            self.ax1.clear()
            self.ax1.grid(True)
            self.ax1.plot(self.x, self.ytarget,  c='r')
            self.ax1.plot(self.x, y,  c='b', linewidth=2)


# Cell
class DynamicEvolver(BaseEvolver):
    "A class to evolve PCT hierarchies from a dynamic structure defintion."
    def __init__(self, env=None, inputs=None, num_actions=1, references=[1], alpha=0.5,
                 mu=0.1, sigma=0.25, indpb=0.2, runs=100,
                 error_response_type=None, error_collector_type=None,
                 error_limit=None, structurepb=0.1, debug=0, seed=None, hpct_verbose=False,
                 nevals=1, file= None, structure=None, error_properties=None, **cargs):
        super().__init__()
        if inputs==None:
            raise Exception('Inputs must be defined')
        self.nevals = nevals
        self.hpct_verbose=hpct_verbose
        self.error_collector_type=error_collector_type
        self.error_response_type=error_response_type
        self.error_limit=error_limit
        if error_collector_type == None:
            self.error_collector_type='TotalError'
        if error_response_type == None:
            self.error_response_type='RootSumSquaredError'
        if error_limit == None:
            self.error_limit=500
        self.env=env
        self.runs=runs
        self.structurepb=structurepb
        self.inputs=inputs
        self.num_actions=num_actions
        self.references=references
        self.debug=debug
        self.seed=seed
        random.seed(seed)
        if self.debug>0:
            print('Initial population')
        self.fig_file=file
        self.member=0
        self.save_arch_all=False
        self.structure=structure
        self.error_properties = error_properties


    def set_save_arch_all(self, save_arch_all):
        self.save_arch_all = save_arch_all

    def create(self, cls):


        #print(grid)
        new_individual = DynamicConfiguration(num_inputs=len(self.inputs), num_actions=self.num_actions,
            references=self.references, structure=self.structure)
        new_individual()
        #for key in new_individual.config.keys():
        #    print(key, new_individual.config[key])
        if self.debug>0:
            print(new_individual.to_raw())
        return cls(new_individual.to_raw())



    def evaluate(self, individual):

        if self.save_arch_all:
            self.fig_file = f'arch-{self.gen:04}-{self.member:04}.png'

        config = BaseConfiguration.from_raw(individual)

        #print(config)
        score = 0

        top_inputs = top_inputs = self.structure.get_parameter('top_inputs')

        for i in range(self.nevals):
            error_collector = BaseErrorCollector.collector(self.error_response_type, self.error_collector_type,
                                                           self.error_limit, properties=self.error_properties)
            if self.seed != None:
                self.env.set_seed(self.seed+i)
            self.env.reset()
            if i > 0:
                self.env.set_render(False)

            pa = DynamicArchitecture(structure=self.structure.arch_structure, config=config, env=self.env,
                                     input_indexes=self.inputs, top_input_indexes=top_inputs, error_collector=error_collector)
            pa()
            hpct = pa.get_hierarchy()

            #hpct.summary()

            if self.fig_file != None:
                hpct.draw(file=self.fig_file)

            hpct.run(steps=self.runs, verbose=self.hpct_verbose)
            score +=  hpct.get_error_collector().error()

        score = score / self.nevals
        if self.debug>1:
            print(f'{self.gen:03} {self.member:03} score {score:5.3f}', config)

        self.member+=1

        return score,



    def mate(self, ind1, ind2):
        child1, child2 = [self.toolbox.clone(ind) for ind in (ind1, ind2)]
        #print('c1', child1)
        #print('c2', child2)

        child1levels = len(child1)
        child2levels = len(child2)

        # actions, at level 0
        level=0
        for item in range(len(child1[level][ControlUnitIndices.ACT_INDEX])):
            self.structure.mate_lists(LevelKey.ZERO , 'action',  child1[level][ControlUnitIndices.ACT_INDEX][item], child2[level][ControlUnitIndices.ACT_INDEX][item])

        # perceptions and outputs at all levels
        for level in range(min(child1levels,child2levels)):
            level_key = self.get_level_key(level, child1levels)
            for item in range(min(len(child1[level][ControlUnitIndices.PER_INDEX]), len(child2[level][ControlUnitIndices.PER_INDEX]))):
                self.structure.mate_lists(level_key, 'perception',  child1[level][ControlUnitIndices.PER_INDEX][item],child2[level][ControlUnitIndices.PER_INDEX][item])

            data_type,x = self.structure.get_type(level_key, 'perception')
            if(data_type == 'Binary'):
                self.structure.ensure_non_zero(level_key, 'perception', child1[level][ControlUnitIndices.PER_INDEX])
                self.structure.ensure_non_zero(level_key, 'perception', child2[level][ControlUnitIndices.PER_INDEX])

            self.structure.mate_lists(level_key, 'output',  child1[level][ControlUnitIndices.OUT_INDEX], child2[level][ControlUnitIndices.OUT_INDEX])

        # references at all levels except top
        for level in range(min(child1levels-1,child2levels-1)):
            level_key = self.get_level_key(level, child1levels)
            #print(level, child1levels, level_key)
            for item in range(min(len(child1[level][ControlUnitIndices.REF_INDEX]), len(child2[level][ControlUnitIndices.REF_INDEX]))):
                self.structure.mate_lists(level_key, 'reference',  child1[level][ControlUnitIndices.REF_INDEX][item],child2[level][ControlUnitIndices.REF_INDEX][item])


        if self.debug>2:
            print('mate b4',ind1)
            print('mate b4',ind2)
            print('mate b5',child1)
            print('mate b5',child2)

        return child1, child2

    def mutate(self, ind, indpb=0.0):
        mutated=False
        mutated_structure = 0
        mutant = self.toolbox.clone(ind)

        if self.debug>2:
            print('mut b4')#,mutant)
            for i in range(len(mutant)):
                print(i, mutant[i])

        levels = len(mutant)

        level=0
        for item in range(len(mutant[level][ControlUnitIndices.PER_INDEX])):
            self.structure.mutate_list(LevelKey.ZERO , 'perception',  mutant[level][ControlUnitIndices.PER_INDEX][item])
        self.structure.mutate_list(LevelKey.ZERO , 'output',  mutant[level][ControlUnitIndices.OUT_INDEX])

        for item in range(len(mutant[level][ControlUnitIndices.ACT_INDEX])):
            self.structure.mutate_list(LevelKey.ZERO , 'action',  mutant[level][ControlUnitIndices.ACT_INDEX][item])



        if levels >1:
            for item in range(len(mutant[level][ControlUnitIndices.REF_INDEX])):
                self.structure.mutate_list(LevelKey.ZERO , 'reference',  mutant[level][ControlUnitIndices.REF_INDEX][item])
            if levels > 2:
                for level in range(1, levels-2, 1):
                    for item in range(len(mutant[level][ControlUnitIndices.PER_INDEX])):
                        self.structure.mutate_list(LevelKey.N, 'perception',  mutant[level][ControlUnitIndices.PER_INDEX][item])

                    self.structure.mutate_list(LevelKey.N, 'output',  mutant[level][ControlUnitIndices.OUT_INDEX])

                    for item in range(len(mutant[level][ControlUnitIndices.REF_INDEX])):
                        self.structure.mutate_list(LevelKey.N, 'reference',  mutant[level][ControlUnitIndices.REF_INDEX][item])

            toplevel=levels-1

            if self.structure.get_config_parameter(LevelKey.TOP, 'perception', 'ones') == BinaryOnes.AT_LEAST_ONE and len(mutant[toplevel][ControlUnitIndices.PER_INDEX][0])==1:
                self.structure.mutate_binary_lists(LevelKey.TOP, 'perception',mutant[toplevel][ControlUnitIndices.PER_INDEX])
            else:
                for item in range(len(mutant[toplevel][ControlUnitIndices.PER_INDEX])):
                    self.structure.mutate_list(LevelKey.TOP, 'perception',  mutant[toplevel][ControlUnitIndices.PER_INDEX][item])

            self.structure.mutate_list(LevelKey.TOP, 'output',  mutant[toplevel][ControlUnitIndices.OUT_INDEX])

        if random.random() < self.structurepb:
            mutant, mutated_structure = self.mutate_structure(mutant)

        if ind != mutant:
            mutated=True

        if mutated or mutated_structure>0:
            self.mutate_sum += 1

        if mutated_structure>0:
            self.mutate_structure_sum += 1

        if self.debug>2:
            print('mut b5')#,mutant)
            for i in range(len(mutant)):
                print(i, mutant[i])

        return mutant,




    def mutate_structure(self, mutant):
        mutated_structure = 0
        raw=mutant
        #print('b4 mutate_structure')
        #for i in range(len(mutant)):
        #    print(i, mutant[i])

        orig_config=BaseConfiguration.from_raw(raw)
        cs = DynamicConfigurationStructure()

        struct = cs.from_raw(raw)
        #print()
        #print(struct)

        choice = random.randint(1,4)
        #print('choice',choice)
        levels = len(struct['grid'])
        if choice == 1 :
            upper_levels_limit = levels-2
            if upper_levels_limit >= 0:
                level = random.randint(0, levels-2)
                max_columns_limit = self.structure.get_parameter('columns_limit')
                columns = struct['grid'][level]
                if columns < max_columns_limit:
                    num_nodes = random.randint(1, max_columns_limit-columns)
                    struct, mutated_structure = cs.add_nodes(level=level, num_nodes=num_nodes)

        if choice == 2 :
            max_levels_limit = self.structure.get_parameter('levels_limit')
            if levels < max_levels_limit:
                num_columns = random.randint(1, self.structure.get_parameter('columns_limit'))
                struct, mutated_structure = cs.add_level(num_columns=num_columns)

        if choice == 3 :
            min_levels_limit = self.structure.get_parameter('min_levels_limit')
            if levels > min_levels_limit:
                struct, mutated_structure = cs.remove_level()

        if choice == 4 :
            upper_levels_limit = levels-2
            if upper_levels_limit >= 0:
                remove_level = random.randint(0, upper_levels_limit)
                upper_node_limit = struct['grid'][remove_level]-1
                min_columns_limit = self.structure.get_parameter('min_columns_limit')
                columns = struct['grid'][remove_level]
                if upper_node_limit > 1:
                    if columns > min_columns_limit:
                        remove_nodes = random.randint(1, upper_node_limit)
                        #print(remove_level, remove_nodes)
                        struct, mutated_structure = cs.remove_nodes(level=remove_level, num_nodes=remove_nodes)

        #print(struct)

        pc=DynamicConfiguration(inputs=self.inputs, structure=self.structure, **struct)
        #pc=DynamicConfiguration( structure=self.structure, **struct)
        config=pc()
        #print('oc',orig_config)
        #print('c ',config)

        if mutated_structure==0 or (choice == 3 and len(orig_config) == len(config)):
            config=orig_config
        else:
            for item in range(len(orig_config['level0'][ControlUnitIndices.ACT_INDEX])):
                self.structure.copy_data(LevelKey.ZERO, 'action',  orig_config['level0'][ControlUnitIndices.ACT_INDEX][item], config['level0'][ControlUnitIndices.ACT_INDEX][item])

            from_top_level =  max(BaseConfiguration.top_level(orig_config), 0)
            to_top_level =  max(BaseConfiguration.top_level(config), 0)
            self.structure.copy_data(LevelKey.TOP, 'reference',  orig_config[f'level{from_top_level}'][ControlUnitIndices.REF_INDEX], config[f'level{to_top_level}'][ControlUnitIndices.REF_INDEX])


            for level in range(from_top_level+1):
                if level < to_top_level+1:
                    level_key = self.get_level_key(level, from_top_level+1)
                    """
                    if level == to_top_level:
                        level_key = 'leveltop'
                    elif level==0 :
                        level_key = 'level0'
                    else:
                        level_key = 'leveln'
                    """
                    for item in range(min(len(orig_config[f'level{level}'][ControlUnitIndices.PER_INDEX]), len(config[f'level{level}'][ControlUnitIndices.PER_INDEX]))):
                        self.structure.copy_data(level_key, 'perception',  orig_config[f'level{level}'][ControlUnitIndices.PER_INDEX][item], config[f'level{level}'][ControlUnitIndices.PER_INDEX][item])

                    self.structure.copy_data(level_key, 'output',  orig_config[f'level{level}'][ControlUnitIndices.OUT_INDEX], config[f'level{level}'][ControlUnitIndices.OUT_INDEX])

                    for item in range(min(len(orig_config[f'level{level}'][ControlUnitIndices.REF_INDEX]),len(config[f'level{level}'][ControlUnitIndices.REF_INDEX]))):
                        if level != from_top_level and level != to_top_level:
                            #print('>',rindex,item, '<',orig_config[f'level{level}'])
                            #print('>',rindex,item, '<',config[f'level{level}'])
                            self.structure.copy_data(level_key, 'reference',  orig_config[f'level{level}'][ControlUnitIndices.REF_INDEX][item], config[f'level{level}'][ControlUnitIndices.REF_INDEX][item])

            mutant = creator.Individual(BaseConfiguration.dict_to_raw(config))

        return mutant, mutated_structure




    def create_plot(self):
        return 0

    def animate(self, epoch):
        pass



# Cell
def check_hash_file_exists(dir_path, hash_str):
    for file in os.listdir(dir_path):
        if file.find(hash_str)>=0:
            return True, file
    return False, None

# Cell
def evolve_from_properties_file(root_dir='.', path='.', file=None, verbose=None, env_name=None, seed=None, test=False,
        gens=None, pop_size=None, nevals = None, move=None, out_dir=None, draw=False, parallel=False, video_wrap=False,
        log=False, figsize=(12,12), summary=False, print_properties=False, overwrite=False, output=False):

    properties = load_properties(root_dir, path, file, print_properties=print_properties, evolve=True)

    modes_list = properties['modes']
    attr_mut_pb = properties['attr_mut_pb']

    lower_float= properties['lower_float']
    upper_float= properties['upper_float']
    levels_limit= properties['levels_limit']
    columns_limit= properties['columns_limit']
    min_levels_limit= properties['min_levels_limit']
    min_columns_limit= properties['min_columns_limit']
    references= properties['references']
    types_strings = properties['types_strings']
    configs_strings = properties['configs_strings']
    error_properties = properties['error_properties']
    if nevals == None:
        nevals = properties['nevals']

    # = properties['']
    # = properties['']

    # arch structure
    modes = {LevelKey.ZERO:modes_list[0], LevelKey.N:modes_list[1],LevelKey.TOP:modes_list[2],LevelKey.ZEROTOP :modes_list[3]}
    arch_structure = ArchitectureStructure(modes=modes)

    # structure
    sdargs={'attr_mut_pb':attr_mut_pb, 'lower_float':lower_float,
            'upper_float':upper_float, 'levels_limit': levels_limit,
            'columns_limit': columns_limit, 'min_levels_limit': min_levels_limit,
            'min_columns_limit': min_columns_limit}
    #print(sdargs)
    structure = StructureDefinition(references=references,**sdargs)
    structure.arch_structure=arch_structure

    types_string=""
    for type_key in types_strings.keys():
        types_string+=types_strings[type_key]
        type_list = stringListToListOfStrings(types_strings[type_key], ',')
        lk = eval(type_list[0])
        #print(lk, type_list[1], type_list[2])
        structure.set_config_type(lk, type_list[1], type_list[2])

    configs_string=""
    for config_key  in configs_strings.keys():
        configs_string+=configs_strings[config_key]
        config_list = stringListToListOfStrings(configs_strings[config_key],',')
        lk = eval(config_list[0])
        bk = eval(config_list[3])
        #print(lk, config_list[1], config_list[2], bk)
        structure.set_config_parameter(lk, config_list[1], config_list[2], bk)

    properties_string=""
    #print(error_properties)
    for property in error_properties:
        property_string = property[0]+property[1]
        properties_string+=property_string

    #print(structure.get_config())

    # evolve arguments
    if pop_size == None:
        pop_size = properties['pop_size']

    if gens == None:
        gens = properties['gens']

    if seed == None:
        seed = properties['seed']

    # env factory
    if env_name==None:
        env_name = properties['env_name']
    env = EnvironmentFactory.createEnvironment(env_name)
    env.set_name(env_name)
    if video_wrap:
        env.set_video_wrap(video_wrap)
        env.create_env(seed)

    if 'early_termination' in properties.keys():
        early_termination = properties['early_termination']
        env.early_termination = early_termination


    error_collector = properties['error_collector_type']
    error_collector_string=error_collector

    error_response=properties['error_response_type']
    error_response_string=error_response

    debug = 0
    if 'debug' in verbose.keys():
        debug = verbose['debug']
    hpct_verbose = False
    if 'hpct_verbose' in verbose.keys():
        hpct_verbose = verbose['hpct_verbose']

    save_arch_all = False
    if 'save_arch_all'  in verbose.keys():
        save_arch_all=verbose['save_arch_all']
    save_arch_gen = False
    if 'save_arch_gen' in verbose.keys():
        save_arch_gen=verbose['save_arch_gen']
    display_env = False
    if 'display_env' in verbose.keys():
        display_env = verbose['display_env']

    evolve_verbose=False
    if 'evolve_verbose' in  verbose.keys():
        evolve_verbose =verbose['evolve_verbose']

    deap_verbose=False
    if 'deap_verbose' in verbose.keys():
        deap_verbose=verbose['deap_verbose']

    inputs = properties['inputs']
    references = properties['references']
    top_inputs=None
    if 'top_inputs' in properties.keys():
        top_inputs=properties['top_inputs']
        structure.set_structure_parameter('top_inputs', top_inputs)

    desc = properties['desc']
    # create hash
    #print(modes_list)
    modes_string = listNumsToString(modes_list)
    hash_string =  f'{inputs}{references}{top_inputs}{properties_string}{desc}{error_response_string}{error_collector_string}{types_string}{modes_string}{seed}{pop_size}{gens}{properties["attr_mut_pb"]}{properties["structurepb"]}{properties["runs"]}{properties["lower_float"]}{properties["upper_float"]}{properties["levels_limit"]}{properties["columns_limit"]}{properties["min_levels_limit"]}{properties["min_columns_limit"]}{properties["error_limit"]}{properties["p_crossover"]}{properties["p_mutation"]}'
    #print(hash_string)
    hash_num = hashlib.md5(hash_string.encode()).hexdigest()
    #print(hash_num)

    if output:
        dir1 = out_dir + env_name
        if not os.path.isdir(dir1):
            os.mkdir(dir1)
        dir2 = dir1+os.sep+desc
        if not os.path.isdir(dir2):
            os.mkdir(dir2)
        exists, fname = check_hash_file_exists(dir2, hash_num)
        if exists and not overwrite:
            print(f'Skipping file {fname}')
            return None,None,None

    evargs={'inputs': inputs, 'env':env, 'num_actions':int(properties['num_actions']), 'nevals':nevals,
            'error_collector_type':error_collector, 'error_response_type':error_response,
            'references': references, 'structure':structure, 'structurepb':float(properties['structurepb']),
            'error_limit':float(properties['error_limit']), 'runs':int(properties['runs']), 'debug':debug,
            'seed':seed, 'hpct_verbose':hpct_verbose, 'error_properties' : error_properties}
    #print(evargs)
    stdev = DynamicEvolver(**evargs)

    evwargs={'evolver':stdev, 'pop_size':pop_size, 'p_crossover': float(properties['p_crossover']),
             'p_mutation': float(properties['p_mutation']), 'save_arch_all': save_arch_all, 'save_arch_gen':save_arch_gen,
             'display_env':display_env, 'select':{'selection_type':'tournament'} ,'parallel':parallel}
    #print(evwargs)
    evr = EvolverWrapper(**evwargs)

    if test:
        #meantime = evr.run(gens=int(db['MAX_GENERATIONS']), verbose=verbose['evolve_verbose'], deap_verbose=verbose['deap_verbose'])
        raw= [777] #evr.best()
        score = 47.2345 #evr.best_score()
        print("Best Score: %0.5f" % score)
        print("Best Ind: ", raw)
        #print(f'Mean time: {meantime:6.3f}')

    else:
        meantime, log_string = evr.run(gens=gens, verbose=evolve_verbose, deap_verbose=deap_verbose, log=log)
        raw= evr.best()
        score = evr.best_score()
        s1 = f'Best Score: {score:0.5f}'
        s2 = f'Best Ind: {raw}'
        s3 = f'Mean time: {meantime:6.3f}'
        if log:
            log_string = ''.join((log_string, '# ', s1, '\n# ', s2, '\n# ', s3, '\n'))
        if evolve_verbose:
            print(s1)
            print(s2)
            print(s3)

        # draw architecture
        if draw:
            inputs_names = None
            if 'inputs_names' in properties.keys():
                inputs_names = properties['inputs_names']
            if move == None:
                move={}
            DynamicArchitecture.draw_raw(raw, arch_structure=arch_structure, move=move, env=env, inputs_names=inputs_names,
                     inputs=inputs, top_input_indexes=top_inputs, figsize=figsize, summary=summary)

    # write results
    output_file = None
    if output:
        cs = DynamicConfigurationStructure()
        struct = cs.from_raw(raw)
        levels = len(struct['grid'])
        cols = max(struct['grid'])
        if seed == None:
            seed = 'N'
        f = open(file, "r")
        file_contents = f.read()

        output_file = dir2+os.sep +f'ga-{score:07.3f}-s{seed:03}-{levels}x{cols}-' + modes_string +f'-{hash_num}.properties'
        if print_properties:
            print(output_file)
        f = open(output_file, "w")
        dateTimeObj = datetime.now()
        f.write(f'# Date {dateTimeObj}\n')
        f.write('# Result'+'\n')
        f.write('# Best individual'+'\n')
        f.write(f'raw = {raw}'+'\n')
        f.write(f'score = {score:0.5f}'+'\n')
        f.write(f'# Time  {meantime:0.4f}'+'\n')
        f.write(file_contents.replace(f'seed = {int(properties["seed"])}', f'seed = {seed}')+'\n')
        if log:
            f.write(log_string)

        f.close()
    env.close()

    return output_file, evr, score
