source: framspy/FramsticksEvolution.py @ 1276

Last change on this file since 1276 was 1205, checked in by Maciej Komosinski, 21 months ago

Splitting (potentially) multiple .sim files is already done by FramsticksLib?.py

File size: 10.2 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(genotype, dict_criteria_values, criterion_name, constraint_value):
13        REPORT_CONSTRAINT_VIOLATIONS = False
14        if constraint_value is not None:
15                actual_value = dict_criteria_values[criterion_name]
16                if actual_value > constraint_value:
17                        if REPORT_CONSTRAINT_VIOLATIONS:
18                                print('Genotype "%s" assigned low fitness because it violates constraint "%s": %s exceeds threshold %s' % (genotype, criterion_name, actual_value, constraint_value))
19                        return False
20        return True
21
22
23def frams_evaluate(frams_lib, individual):
24        BAD_FITNESS = [-1] * len(OPTIMIZATION_CRITERIA)  # fitness of -1 is intended to discourage further propagation of this genotype via selection ("this genotype is very poor")
25        genotype = individual[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
26        data = frams_lib.evaluate([genotype])
27        # print("Evaluated '%s'" % genotype, 'evaluation is:', data)
28        valid = True
29        try:
30                first_genotype_data = data[0]
31                evaluation_data = first_genotype_data["evaluations"]
32                default_evaluation_data = evaluation_data[""]
33                fitness = [default_evaluation_data[crit] for crit in OPTIMIZATION_CRITERIA]
34        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
35                valid = False
36                print('Problem "%s" so could not evaluate genotype "%s", hence assigned it low fitness: %s' % (str(e), genotype, BAD_FITNESS))
37        if valid:
38                default_evaluation_data['numgenocharacters'] = len(genotype)  # for consistent constraint checking below
39                valid &= genotype_within_constraint(genotype, default_evaluation_data, 'numparts', parsed_args.max_numparts)
40                valid &= genotype_within_constraint(genotype, default_evaluation_data, 'numjoints', parsed_args.max_numjoints)
41                valid &= genotype_within_constraint(genotype, default_evaluation_data, 'numneurons', parsed_args.max_numneurons)
42                valid &= genotype_within_constraint(genotype, default_evaluation_data, 'numconnections', parsed_args.max_numconnections)
43                valid &= genotype_within_constraint(genotype, default_evaluation_data, 'numgenocharacters', parsed_args.max_numgenochars)
44        if not valid:
45                fitness = BAD_FITNESS
46        return fitness
47
48
49def frams_crossover(frams_lib, individual1, individual2):
50        geno1 = individual1[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
51        geno2 = individual2[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
52        individual1[0] = frams_lib.crossOver(geno1, geno2)
53        individual2[0] = frams_lib.crossOver(geno1, geno2)
54        return individual1, individual2
55
56
57def frams_mutate(frams_lib, individual):
58        individual[0] = frams_lib.mutate([individual[0]])[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
59        return individual,
60
61
62def frams_getsimplest(frams_lib, genetic_format, initial_genotype):
63        return initial_genotype if initial_genotype is not None else frams_lib.getSimplest(genetic_format)
64
65
66def prepareToolbox(frams_lib, OPTIMIZATION_CRITERIA, tournament_size, genetic_format, initial_genotype):
67        creator.create("FitnessMax", base.Fitness, weights=[1.0] * len(OPTIMIZATION_CRITERIA))
68        creator.create("Individual", list, fitness=creator.FitnessMax)  # would be nice to have "str" instead of unnecessary "list of str"
69
70        toolbox = base.Toolbox()
71        toolbox.register("attr_simplest_genotype", frams_getsimplest, frams_lib, genetic_format, initial_genotype)  # "Attribute generator"
72        # (failed) struggle to have an individual which is a simple str, not a list of str
73        # toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_frams)
74        # https://stackoverflow.com/questions/51451815/python-deap-library-using-random-words-as-individuals
75        # https://github.com/DEAP/deap/issues/339
76        # https://gitlab.com/santiagoandre/deap-customize-population-example/-/blob/master/AGbasic.py
77        # https://groups.google.com/forum/#!topic/deap-users/22g1kyrpKy8
78        toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_simplest_genotype, 1)
79        toolbox.register("population", tools.initRepeat, list, toolbox.individual)
80        toolbox.register("evaluate", frams_evaluate, frams_lib)
81        toolbox.register("mate", frams_crossover, frams_lib)
82        toolbox.register("mutate", frams_mutate, frams_lib)
83        if len(OPTIMIZATION_CRITERIA) <= 1:
84                toolbox.register("select", tools.selTournament, tournsize=tournament_size)
85        else:
86                toolbox.register("select", tools.selNSGA2)
87        return toolbox
88
89
90def parseArguments():
91        parser = argparse.ArgumentParser(description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[0])
92        parser.add_argument('-path', type=ensureDir, required=True, help='Path to Framsticks library without trailing slash.')
93        parser.add_argument('-lib', required=False, help='Library name. If not given, "frams-objects.dll" (or .so or .dylib) is assumed depending on the platform.')
94        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 ';'.")
95
96        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.')
97        parser.add_argument('-initialgenotype', required=False, help='The genotype used to seed the initial population. If given, the -genformat argument is ignored.')
98
99        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.')
100        parser.add_argument('-popsize', type=int, default=50, help="Population size, default: 50.")
101        parser.add_argument('-generations', type=int, default=5, help="Number of generations, default: 5.")
102        parser.add_argument('-tournament', type=int, default=5, help="Tournament size, default: 5.")
103        parser.add_argument('-pmut', type=float, default=0.9, help="Probability of mutation, default: 0.9")
104        parser.add_argument('-pxov', type=float, default=0.2, help="Probability of crossover, default: 0.2")
105        parser.add_argument('-hof_size', type=int, default=10, help="Number of genotypes in Hall of Fame. Default: 10.")
106        parser.add_argument('-hof_savefile', required=False, help='If set, Hall of Fame will be saved in Framsticks file format (recommended extension *.gen).')
107
108        parser.add_argument('-max_numparts', type=int, default=None, help="Maximum number of Parts. Default: no limit")
109        parser.add_argument('-max_numjoints', type=int, default=None, help="Maximum number of Joints. Default: no limit")
110        parser.add_argument('-max_numneurons', type=int, default=None, help="Maximum number of Neurons. Default: no limit")
111        parser.add_argument('-max_numconnections', type=int, default=None, help="Maximum number of Neural connections. Default: no limit")
112        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")
113        return parser.parse_args()
114
115
116def ensureDir(string):
117        if os.path.isdir(string):
118                return string
119        else:
120                raise NotADirectoryError(string)
121
122
123def save_genotypes(filename, OPTIMIZATION_CRITERIA, hof):
124        from framsfiles import writer as framswriter
125        with open(filename, "w") as outfile:
126                for ind in hof:
127                        keyval = {}
128                        for i, k in enumerate(OPTIMIZATION_CRITERIA):  # construct a dictionary with criteria names and their values
129                                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.
130                        # 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).
131                        outfile.write(framswriter.from_collection({"_classname": "org", "genotype": ind[0], **keyval}))
132                        outfile.write("\n")
133        print("Saved '%s' (%d)" % (filename, len(hof)))
134
135
136def main():
137        global parsed_args, OPTIMIZATION_CRITERIA  # needed in frams_evaluate(), so made global to avoid passing as arguments
138
139        # random.seed(123)  # see FramsticksLib.DETERMINISTIC below, set to True if you want full determinism
140        FramsticksLib.DETERMINISTIC = False  # must be set before FramsticksLib() constructor call
141        parsed_args = parseArguments()
142        print("Argument values:", ", ".join(['%s=%s' % (arg, getattr(parsed_args, arg)) for arg in vars(parsed_args)]))
143        OPTIMIZATION_CRITERIA = parsed_args.opt.split(",")
144        framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.sim)
145        toolbox = prepareToolbox(framsLib, OPTIMIZATION_CRITERIA, parsed_args.tournament, '1' if parsed_args.genformat is None else parsed_args.genformat, parsed_args.initialgenotype)
146        pop = toolbox.population(n=parsed_args.popsize)
147        hof = tools.HallOfFame(parsed_args.hof_size)
148        stats = tools.Statistics(lambda ind: ind.fitness.values)
149        stats.register("avg", np.mean)
150        stats.register("stddev", np.std)
151        stats.register("min", np.min)
152        stats.register("max", np.max)
153        pop, log = algorithms.eaSimple(pop, toolbox, cxpb=parsed_args.pxov, mutpb=parsed_args.pmut, ngen=parsed_args.generations, stats=stats, halloffame=hof, verbose=True)
154        print('Best individuals:')
155        for ind in hof:
156                print(ind.fitness, '\t-->\t', ind[0])
157        if parsed_args.hof_savefile is not None:
158                save_genotypes(parsed_args.hof_savefile, OPTIMIZATION_CRITERIA, hof)
159
160
161if __name__ == "__main__":
162        main()
Note: See TracBrowser for help on using the repository browser.