source: framspy/FramsticksEvolution.py @ 1163

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

Improved diagnostic messages

File size: 10.0 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_cli, 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_cli.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_cli, 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_cli.crossOver(geno1, geno2)
53        individual2[0] = frams_cli.crossOver(geno1, geno2)
54        return individual1, individual2
55
56
57def frams_mutate(frams_cli, individual):
58        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.
59        return individual,
60
61
62def frams_getsimplest(frams_cli, genetic_format, initial_genotype):
63        return initial_genotype if initial_genotype is not None else frams_cli.getSimplest(genetic_format)
64
65
66def prepareToolbox(frams_cli, 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_cli, 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_cli)
81        toolbox.register("mate", frams_crossover, frams_cli)
82        toolbox.register("mutate", frams_mutate, frams_cli)
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 CLI without trailing slash.')
93        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.')
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
136if __name__ == "__main__":
137        # random.seed(123)  # see FramsticksLib.DETERMINISTIC below, set to True if you want full determinism
138        FramsticksLib.DETERMINISTIC = False  # must be set before FramsticksLib() constructor call
139        parsed_args = parseArguments()
140        print("Argument values:", ", ".join(['%s=%s' % (arg, getattr(parsed_args, arg)) for arg in vars(parsed_args)]))
141
142        OPTIMIZATION_CRITERIA = parsed_args.opt.split(",")
143        framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.sim.split(";"))
144
145        toolbox = prepareToolbox(framsLib, parsed_args.tournament, '1' if parsed_args.genformat is None else parsed_args.genformat, parsed_args.initialgenotype)
146
147        pop = toolbox.population(n=parsed_args.popsize)
148        hof = tools.HallOfFame(parsed_args.hof_size)
149        stats = tools.Statistics(lambda ind: ind.fitness.values)
150        stats.register("avg", np.mean)
151        stats.register("stddev", np.std)
152        stats.register("min", np.min)
153        stats.register("max", np.max)
154
155        pop, log = algorithms.eaSimple(pop, toolbox, cxpb=parsed_args.pxov, mutpb=parsed_args.pmut, ngen=parsed_args.generations, stats=stats, halloffame=hof, verbose=True)
156        print('Best individuals:')
157        for ind in hof:
158                print(ind.fitness, '\t-->\t', ind[0])
159
160        if parsed_args.hof_savefile is not None:
161                save_genotypes(parsed_args.hof_savefile, OPTIMIZATION_CRITERIA, hof)
Note: See TracBrowser for help on using the repository browser.