1 | from ..base.experiment_abc import ExperimentABC |
---|
2 | from ..constants import BAD_FITNESS |
---|
3 | from ..structures.individual import Individual |
---|
4 | from ..structures.population import PopulationStructures |
---|
5 | from ..utils import ensureDir |
---|
6 | |
---|
7 | |
---|
8 | class ExperimentFrams(ExperimentABC): |
---|
9 | def __init__(self, hof_size, popsize, frams_lib, optimization_criteria, genformat, save_only_best=True, constraints={}) -> None: |
---|
10 | ExperimentABC.__init__(self, hof_size=hof_size, popsize=popsize, save_only_best=save_only_best) |
---|
11 | self.optimization_criteria = optimization_criteria |
---|
12 | self.frams_lib = frams_lib |
---|
13 | self.constraints = constraints |
---|
14 | self.genformat = genformat |
---|
15 | |
---|
16 | def frams_getsimplest(self, genetic_format, initial_genotype): |
---|
17 | return initial_genotype if initial_genotype is not None else self.frams_lib.getSimplest(genetic_format) |
---|
18 | |
---|
19 | def genotype_within_constraint(self, genotype, dict_criteria_values, criterion_name, constraint_value): |
---|
20 | REPORT_CONSTRAINT_VIOLATIONS = False |
---|
21 | if constraint_value is not None: |
---|
22 | actual_value = dict_criteria_values[criterion_name] |
---|
23 | if actual_value > constraint_value: |
---|
24 | if REPORT_CONSTRAINT_VIOLATIONS: |
---|
25 | print('Genotype "%s" assigned low fitness because it violates constraint "%s": %s exceeds threshold %s' % ( |
---|
26 | genotype, criterion_name, actual_value, constraint_value)) |
---|
27 | return False |
---|
28 | return True |
---|
29 | |
---|
30 | def check_valid_constraints(self, genotype, default_evaluation_data): |
---|
31 | valid = True |
---|
32 | valid &= self.genotype_within_constraint( |
---|
33 | genotype, default_evaluation_data, 'numparts', self.constraints.get('max_numparts')) |
---|
34 | valid &= self.genotype_within_constraint( |
---|
35 | genotype, default_evaluation_data, 'numjoints', self.constraints.get('max_numjoints')) |
---|
36 | valid &= self.genotype_within_constraint( |
---|
37 | genotype, default_evaluation_data, 'numneurons', self.constraints.get('max_numneurons')) |
---|
38 | valid &= self.genotype_within_constraint( |
---|
39 | genotype, default_evaluation_data, 'numconnections', self.constraints.get('max_numconnections')) |
---|
40 | valid &= self.genotype_within_constraint( |
---|
41 | genotype, default_evaluation_data, 'numgenocharacters', self.constraints.get('max_numgenochars')) |
---|
42 | return valid |
---|
43 | |
---|
44 | def evaluate(self, genotype): |
---|
45 | data = self.frams_lib.evaluate([genotype]) |
---|
46 | # print("Evaluated '%s'" % genotype, 'evaluation is:', data) |
---|
47 | valid = True |
---|
48 | try: |
---|
49 | first_genotype_data = data[0] |
---|
50 | evaluation_data = first_genotype_data["evaluations"] |
---|
51 | default_evaluation_data = evaluation_data[""] |
---|
52 | fitness = [default_evaluation_data[crit] for crit in self.optimization_criteria][0] |
---|
53 | # 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 |
---|
54 | except (KeyError, TypeError) as e: |
---|
55 | valid = False |
---|
56 | print('Problem "%s" so could not evaluate genotype "%s", hence assigned it fitness: %s' % ( |
---|
57 | str(e), genotype, BAD_FITNESS)) |
---|
58 | if valid: |
---|
59 | default_evaluation_data['numgenocharacters'] = len(genotype) # for consistent constraint checking below |
---|
60 | valid = self.check_valid_constraints(genotype, default_evaluation_data) |
---|
61 | if not valid: |
---|
62 | fitness = BAD_FITNESS |
---|
63 | return fitness |
---|
64 | |
---|
65 | |
---|
66 | def mutate(self, gen1): |
---|
67 | return self.frams_lib.mutate([gen1])[0] |
---|
68 | |
---|
69 | def cross_over(self, gen1, gen2): |
---|
70 | return self.frams_lib.crossOver(gen1, gen2) |
---|
71 | |
---|
72 | def initialize_evolution(self, initialgenotype): |
---|
73 | self.current_generation = 0 |
---|
74 | self.time_elapsed = 0 |
---|
75 | self.stats = [] # stores the best individuals, one from each generation |
---|
76 | initial_individual = Individual() |
---|
77 | initial_individual.set_and_evaluate(self.frams_getsimplest( |
---|
78 | '1' if self.genformat is None else self.genformat, initialgenotype), self.evaluate) |
---|
79 | self.hof.add(initial_individual) |
---|
80 | self.stats.append( |
---|
81 | initial_individual.rawfitness if self.save_only_best else initial_individual) |
---|
82 | self.population_structures = PopulationStructures( |
---|
83 | initial_individual=initial_individual, popsize=self.popsize) |
---|
84 | |
---|
85 | def save_genotypes(self, filename): |
---|
86 | from framsfiles import writer as framswriter |
---|
87 | with open(filename, "w") as outfile: |
---|
88 | for ind in self.hof: |
---|
89 | keyval = {} |
---|
90 | # construct a dictionary with criteria names and their values |
---|
91 | for i, k in enumerate(self.optimization_criteria): |
---|
92 | # .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. |
---|
93 | keyval[k] = ind.rawfitness |
---|
94 | # 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). |
---|
95 | outfile.write(framswriter.from_collection( |
---|
96 | {"_classname": "org", "genotype": ind.genotype, **keyval})) |
---|
97 | outfile.write("\n") |
---|
98 | print("Saved '%s' (%d)" % (filename, len(self.hof))) |
---|
99 | |
---|
100 | @staticmethod |
---|
101 | def get_args_for_parser(): |
---|
102 | parser = ExperimentABC.get_args_for_parser() |
---|
103 | parser.add_argument('-path',type= ensureDir, required= True, |
---|
104 | help= 'Path to Framsticks CLI without trailing slash.') |
---|
105 | parser.add_argument('-lib',type= str, required= False, |
---|
106 | help= 'Library name. If not given, "frams-objects.dll" or "frams-objects.so" is assumed depending on the platform.') |
---|
107 | parser.add_argument('-sim',type= str, required= False, default= "eval-allcriteria.sim", |
---|
108 | 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 ';'.") |
---|
109 | |
---|
110 | parser.add_argument('-genformat',type= str, required= False, |
---|
111 | help= 'Genetic format for the simplest initial genotype, for example 4, 9, or B. If not given, f1 is assumed.') |
---|
112 | parser.add_argument('-initialgenotype',type= str, required= False, |
---|
113 | help= 'The genotype used to seed the initial population. If given, the -genformat argument is ignored.') |
---|
114 | 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).') |
---|
115 | |
---|
116 | parser.add_argument('-max_numparts',type= int, default= None, |
---|
117 | help="Maximum number of Parts. Default: no limit") |
---|
118 | parser.add_argument('-max_numjoints',type= int, default= None, |
---|
119 | help="Maximum number of Joints. Default: no limit") |
---|
120 | parser.add_argument('-max_numneurons',type= int, default= None, |
---|
121 | help="Maximum number of Neurons. Default: no limit") |
---|
122 | parser.add_argument('-max_numconnections',type= int, default= None, |
---|
123 | help="Maximum number of Neural connections. Default: no limit") |
---|
124 | parser.add_argument('-max_numgenochars',type= int, default= None, |
---|
125 | help="Maximum number of characters in genotype (including the format prefix, if any}. Default: no limit") |
---|
126 | return parser |
---|