[948] | 1 | import argparse
|
---|
| 2 | import os
|
---|
| 3 | import sys
|
---|
| 4 | import numpy as np
|
---|
| 5 | from deap import creator, base, tools, algorithms
|
---|
[1080] | 6 | from FramsticksLib import FramsticksLib
|
---|
[948] | 7 |
|
---|
[1080] | 8 | # Note: this may be less efficient than running the evolution directly in Framsticks, so if performance is key, compare both options.
|
---|
[948] | 9 |
|
---|
| 10 |
|
---|
| 11 | # The list of criteria includes 'vertpos', 'velocity', 'distance', 'vertvel', 'lifespan', 'numjoints', 'numparts', 'numneurons', 'numconnections'.
|
---|
| 12 | OPTIMIZATION_CRITERIA = ['vertpos'] # Single or multiple criteria. Names from the standard-eval.expdef dictionary, e.g. ['vertpos', 'velocity'].
|
---|
| 13 |
|
---|
| 14 |
|
---|
| 15 | def frams_evaluate(frams_cli, individual):
|
---|
[1060] | 16 | genotype = individual[0] # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
|
---|
[1057] | 17 | data = frams_cli.evaluate([genotype])
|
---|
[948] | 18 | # print("Evaluated '%s'" % genotype, 'evaluation is:', data)
|
---|
| 19 | try:
|
---|
| 20 | first_genotype_data = data[0]
|
---|
| 21 | evaluation_data = first_genotype_data["evaluations"]
|
---|
| 22 | default_evaluation_data = evaluation_data[""]
|
---|
| 23 | fitness = [default_evaluation_data[crit] for crit in OPTIMIZATION_CRITERIA]
|
---|
[1060] | 24 | except (KeyError, TypeError) as e: # the evaluation may have failed for an invalid genotype (such as X[@][@] with "Don't simulate genotypes with warnings" option) or for some other reason
|
---|
| 25 | fitness = [-1] * len(OPTIMIZATION_CRITERIA) # fitness of -1 is intended to discourage further propagation of this genotype via selection ("this one is very poor")
|
---|
[948] | 26 | print("Error '%s': could not evaluate genotype '%s', returning fitness %s" % (str(e), genotype, fitness))
|
---|
| 27 | return fitness
|
---|
| 28 |
|
---|
| 29 |
|
---|
| 30 | def frams_crossover(frams_cli, individual1, individual2):
|
---|
[1060] | 31 | geno1 = individual1[0] # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
|
---|
| 32 | geno2 = individual2[0] # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
|
---|
[948] | 33 | individual1[0] = frams_cli.crossOver(geno1, geno2)
|
---|
| 34 | individual2[0] = frams_cli.crossOver(geno1, geno2)
|
---|
| 35 | return individual1, individual2
|
---|
| 36 |
|
---|
| 37 |
|
---|
| 38 | def frams_mutate(frams_cli, individual):
|
---|
[1060] | 39 | individual[0] = frams_cli.mutate([individual[0]])[0] # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
|
---|
[948] | 40 | return individual,
|
---|
| 41 |
|
---|
| 42 |
|
---|
| 43 | def frams_getsimplest(frams_cli, genetic_format):
|
---|
| 44 | return frams_cli.getSimplest(genetic_format)
|
---|
| 45 |
|
---|
| 46 |
|
---|
| 47 | def prepareToolbox(frams_cli, genetic_format):
|
---|
| 48 | creator.create("FitnessMax", base.Fitness, weights=[1.0] * len(OPTIMIZATION_CRITERIA))
|
---|
| 49 | creator.create("Individual", list, fitness=creator.FitnessMax) # would be nice to have "str" instead of unnecessary "list of str"
|
---|
| 50 |
|
---|
| 51 | toolbox = base.Toolbox()
|
---|
| 52 | toolbox.register("attr_simplest_genotype", frams_getsimplest, frams_cli, genetic_format) # "Attribute generator"
|
---|
| 53 | # (failed) struggle to have an individual which is a simple str, not a list of str
|
---|
| 54 | # toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_frams)
|
---|
| 55 | # https://stackoverflow.com/questions/51451815/python-deap-library-using-random-words-as-individuals
|
---|
| 56 | # https://github.com/DEAP/deap/issues/339
|
---|
| 57 | # https://gitlab.com/santiagoandre/deap-customize-population-example/-/blob/master/AGbasic.py
|
---|
| 58 | # https://groups.google.com/forum/#!topic/deap-users/22g1kyrpKy8
|
---|
| 59 | toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_simplest_genotype, 1)
|
---|
| 60 | toolbox.register("population", tools.initRepeat, list, toolbox.individual)
|
---|
| 61 | toolbox.register("evaluate", frams_evaluate, frams_cli)
|
---|
| 62 | toolbox.register("mate", frams_crossover, frams_cli)
|
---|
| 63 | toolbox.register("mutate", frams_mutate, frams_cli)
|
---|
[949] | 64 | if len(OPTIMIZATION_CRITERIA) <= 1:
|
---|
[948] | 65 | toolbox.register("select", tools.selTournament, tournsize=5)
|
---|
| 66 | else:
|
---|
| 67 | toolbox.register("select", tools.selNSGA2)
|
---|
| 68 | return toolbox
|
---|
| 69 |
|
---|
| 70 |
|
---|
| 71 | def parseArguments():
|
---|
| 72 | parser = argparse.ArgumentParser(description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[0])
|
---|
| 73 | parser.add_argument('-path', type=ensureDir, required=True, help='Path to Framsticks CLI without trailing slash.')
|
---|
| 74 | parser.add_argument('-exe', required=False, help='Executable name. If not given, "frams.exe" or "frams.linux" is assumed.')
|
---|
| 75 | parser.add_argument('-genformat', required=False, help='Genetic format for the demo run, for example 4, 9, or B. If not given, f1 is assumed.')
|
---|
| 76 | return parser.parse_args()
|
---|
| 77 |
|
---|
| 78 |
|
---|
| 79 | def ensureDir(string):
|
---|
| 80 | if os.path.isdir(string):
|
---|
| 81 | return string
|
---|
| 82 | else:
|
---|
| 83 | raise NotADirectoryError(string)
|
---|
| 84 |
|
---|
| 85 |
|
---|
| 86 | if __name__ == "__main__":
|
---|
| 87 | # A demo run: optimize OPTIMIZATION_CRITERIA
|
---|
| 88 |
|
---|
[1080] | 89 | # random.seed(123) # see FramsticksLib.DETERMINISTIC below, set to True if you want full determinism
|
---|
| 90 | FramsticksLib.DETERMINISTIC = False # must be set before FramsticksLib() constructor call
|
---|
[948] | 91 | parsed_args = parseArguments()
|
---|
[1080] | 92 | framsLib = FramsticksLib(parsed_args.path, parsed_args.exe)
|
---|
[948] | 93 |
|
---|
[1080] | 94 | toolbox = prepareToolbox(framsLib, '1' if parsed_args.genformat is None else parsed_args.genformat)
|
---|
[948] | 95 |
|
---|
| 96 | POPSIZE = 10
|
---|
| 97 | GENERATIONS = 100
|
---|
| 98 |
|
---|
| 99 | pop = toolbox.population(n=POPSIZE)
|
---|
| 100 | hof = tools.HallOfFame(5)
|
---|
| 101 | stats = tools.Statistics(lambda ind: ind.fitness.values)
|
---|
| 102 | stats.register("avg", np.mean)
|
---|
| 103 | stats.register("stddev", np.std)
|
---|
| 104 | stats.register("min", np.min)
|
---|
| 105 | stats.register("max", np.max)
|
---|
| 106 |
|
---|
| 107 | print('Evolution with population size %d for %d generations, optimization criteria: %s' % (POPSIZE, GENERATIONS, OPTIMIZATION_CRITERIA))
|
---|
[961] | 108 | pop, log = algorithms.eaSimple(pop, toolbox, cxpb=0.2, mutpb=0.9, ngen=GENERATIONS, stats=stats, halloffame=hof, verbose=True)
|
---|
[948] | 109 | print('Best individuals:')
|
---|
| 110 | for best in hof:
|
---|
| 111 | print(best.fitness, '\t-->\t', best[0])
|
---|