source: framspy/FramsticksEvolution.py @ 1161

Last change on this file since 1161 was 1161, checked in by Maciej Komosinski, 6 months ago

Multiple new functionalities in simple evolutionary optimization in python; added many new parameters; added some examples

File size: 9.5 KB
Line 
1import argparse
2import os
3import sys
4import numpy as np
5from deap import creator, base, tools, algorithms
6from FramsticksLib import FramsticksLib
7
8
9# Note: this may be less efficient than running the evolution directly in Framsticks, so if performance is key, compare both options.
10
11
12def genotype_within_constraint(criterion_actual_value, constraint_value):
13        if constraint_value is not None:
14                if criterion_actual_value > constraint_value:
15                        return False
16        return True
17
18
19def frams_evaluate(frams_cli, individual):
20        genotype = individual[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
21        data = frams_cli.evaluate([genotype])
22        # print("Evaluated '%s'" % genotype, 'evaluation is:', data)
23        valid = True
24        try:
25                first_genotype_data = data[0]
26                evaluation_data = first_genotype_data["evaluations"]
27                default_evaluation_data = evaluation_data[""]
28                fitness = [default_evaluation_data[crit] for crit in OPTIMIZATION_CRITERIA]
29        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
30                valid = False
31                print('Error "%s": could not evaluate genotype "%s", returning fitness %s' % (str(e), genotype, fitness))
32        if valid:
33                valid &= genotype_within_constraint(default_evaluation_data['numparts'], parsed_args.max_numparts)
34                valid &= genotype_within_constraint(default_evaluation_data['numjoints'], parsed_args.max_numjoints)
35                valid &= genotype_within_constraint(default_evaluation_data['numneurons'], parsed_args.max_numneurons)
36                valid &= genotype_within_constraint(default_evaluation_data['numconnections'], parsed_args.max_numconnections)
37                valid &= genotype_within_constraint(len(genotype), parsed_args.max_numgenochars)
38        if not valid:
39                fitness = [-1] * len(OPTIMIZATION_CRITERIA)  # fitness of -1 is intended to discourage further propagation of this genotype via selection ("this genotype is very poor")
40        return fitness
41
42
43def frams_crossover(frams_cli, individual1, individual2):
44        geno1 = individual1[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
45        geno2 = individual2[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
46        individual1[0] = frams_cli.crossOver(geno1, geno2)
47        individual2[0] = frams_cli.crossOver(geno1, geno2)
48        return individual1, individual2
49
50
51def frams_mutate(frams_cli, individual):
52        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.
53        return individual,
54
55
56def frams_getsimplest(frams_cli, genetic_format, initial_genotype):
57        return initial_genotype if initial_genotype is not None else frams_cli.getSimplest(genetic_format)
58
59
60def prepareToolbox(frams_cli, tournament_size, genetic_format, initial_genotype):
61        creator.create("FitnessMax", base.Fitness, weights=[1.0] * len(OPTIMIZATION_CRITERIA))
62        creator.create("Individual", list, fitness=creator.FitnessMax)  # would be nice to have "str" instead of unnecessary "list of str"
63
64        toolbox = base.Toolbox()
65        toolbox.register("attr_simplest_genotype", frams_getsimplest, frams_cli, genetic_format, initial_genotype)  # "Attribute generator"
66        # (failed) struggle to have an individual which is a simple str, not a list of str
67        # toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_frams)
68        # https://stackoverflow.com/questions/51451815/python-deap-library-using-random-words-as-individuals
69        # https://github.com/DEAP/deap/issues/339
70        # https://gitlab.com/santiagoandre/deap-customize-population-example/-/blob/master/AGbasic.py
71        # https://groups.google.com/forum/#!topic/deap-users/22g1kyrpKy8
72        toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_simplest_genotype, 1)
73        toolbox.register("population", tools.initRepeat, list, toolbox.individual)
74        toolbox.register("evaluate", frams_evaluate, frams_cli)
75        toolbox.register("mate", frams_crossover, frams_cli)
76        toolbox.register("mutate", frams_mutate, frams_cli)
77        if len(OPTIMIZATION_CRITERIA) <= 1:
78                toolbox.register("select", tools.selTournament, tournsize=tournament_size)
79        else:
80                toolbox.register("select", tools.selNSGA2)
81        return toolbox
82
83
84def parseArguments():
85        parser = argparse.ArgumentParser(description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[0])
86        parser.add_argument('-path', type=ensureDir, required=True, help='Path to Framsticks CLI without trailing slash.')
87        parser.add_argument('-lib', required=False, help='Library name. If not given, "frams-objects.dll" or "frams-objects.so" is assumed depending on the platform.')
88        parser.add_argument('-sim', required=False, default="eval-allcriteria.sim", help="The name of the .sim file with settings for evaluation, mutation, crossover, and similarity estimation. If not given, \"eval-allcriteria.sim\" is assumed by default. Must be compatible with the \"standard-eval\" expdef. If you want to provide more files, separate them with a semicolon ';'.")
89
90        parser.add_argument('-genformat', required=False, help='Genetic format for the simplest initial genotype, for example 4, 9, or B. If not given, f1 is assumed.')
91        parser.add_argument('-initialgenotype', required=False, help='The genotype used to seed the initial population. If given, the -genformat argument is ignored.')
92
93        parser.add_argument('-opt', required=True, help='optimization criteria: vertpos, velocity, distance, vertvel, lifespan, numjoints, numparts, numneurons, numconnections (or other as long as it is provided by the .sim file and its .expdef). For multiple criteria optimization, separate the names by the comma.')
94        parser.add_argument('-popsize', type=int, default=50, help="Population size, default: 50.")
95        parser.add_argument('-generations', type=int, default=5, help="Number of generations, default: 5.")
96        parser.add_argument('-tournament', type=int, default=5, help="Tournament size, default: 5.")
97        parser.add_argument('-pmut', type=float, default=0.9, help="Probability of mutation, default: 0.9")
98        parser.add_argument('-pxov', type=float, default=0.2, help="Probability of crossover, default: 0.2")
99        parser.add_argument('-hof_size', type=int, default=10, help="Number of genotypes in Hall of Fame. Default: 10.")
100        parser.add_argument('-hof_savefile', required=False, help='If set, Hall of Fame will be saved in Framsticks file format (recommended extension *.gen).')
101
102        parser.add_argument('-max_numparts', type=int, default=None, help="Maximum number of Parts. Default: no limit")
103        parser.add_argument('-max_numjoints', type=int, default=None, help="Maximum number of Joints. Default: no limit")
104        parser.add_argument('-max_numneurons', type=int, default=None, help="Maximum number of Neurons. Default: no limit")
105        parser.add_argument('-max_numconnections', type=int, default=None, help="Maximum number of Neural connections. Default: no limit")
106        parser.add_argument('-max_numgenochars', type=int, default=None, help="Maximum number of characters in genotype (including the format prefix, if any). Default: no limit")
107        return parser.parse_args()
108
109
110def ensureDir(string):
111        if os.path.isdir(string):
112                return string
113        else:
114                raise NotADirectoryError(string)
115
116
117def save_genotypes(filename, OPTIMIZATION_CRITERIA, hof):
118        from framsfiles import writer as framswriter
119        with open(filename, "w") as outfile:
120                for ind in hof:
121                        keyval = {}
122                        for i, k in enumerate(OPTIMIZATION_CRITERIA):  # construct a dictionary with criteria names and their values
123                                keyval[k] = ind.fitness.values[i]  # TODO it would be better to save in Individual (after evaluation) all fields returned by Framsticks, and get these fields here, not just the criteria that were actually used as fitness in evolution.
124                        # Note: prior to the release of Framsticks 5.0, saving e.g. numparts (i.e. P) without J,N,C breaks re-calcucation of P,J,N,C in Framsticks and they appear to be zero (nothing serious).
125                        outfile.write(framswriter.from_collection({"_classname": "org", "genotype": ind[0], **keyval}))
126                        outfile.write("\n")
127        print("Saved '%s' (%d)" % (filename, len(hof)))
128
129
130if __name__ == "__main__":
131        # random.seed(123)  # see FramsticksLib.DETERMINISTIC below, set to True if you want full determinism
132        FramsticksLib.DETERMINISTIC = False  # must be set before FramsticksLib() constructor call
133        parsed_args = parseArguments()
134        print("Argument values:", ", ".join(['%s=%s' % (arg, getattr(parsed_args, arg)) for arg in vars(parsed_args)]))
135
136        OPTIMIZATION_CRITERIA = parsed_args.opt.split(",")
137        framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.sim.split(";"))
138
139        toolbox = prepareToolbox(framsLib, parsed_args.tournament, '1' if parsed_args.genformat is None else parsed_args.genformat, parsed_args.initialgenotype)
140
141        pop = toolbox.population(n=parsed_args.popsize)
142        hof = tools.HallOfFame(parsed_args.hof_size)
143        stats = tools.Statistics(lambda ind: ind.fitness.values)
144        stats.register("avg", np.mean)
145        stats.register("stddev", np.std)
146        stats.register("min", np.min)
147        stats.register("max", np.max)
148
149        pop, log = algorithms.eaSimple(pop, toolbox, cxpb=parsed_args.pxov, mutpb=parsed_args.pmut, ngen=parsed_args.generations, stats=stats, halloffame=hof, verbose=True)
150        print('Best individuals:')
151        for ind in hof:
152                print(ind.fitness, '\t-->\t', ind[0])
153
154        if parsed_args.hof_savefile is not None:
155                save_genotypes(parsed_args.hof_savefile, OPTIMIZATION_CRITERIA, hof)
Note: See TracBrowser for help on using the repository browser.