source: framspy/evolalg/frams_base/experiment_frams.py @ 1190

Last change on this file since 1190 was 1190, checked in by Maciej Komosinski, 17 months ago

Added the "evolalg" module for evolutionary optimization

File size: 7.7 KB
Line 
1from ..base.experiment_abc import ExperimentABC
2from ..constants import BAD_FITNESS
3from ..structures.individual import Individual
4from ..structures.population import PopulationStructures
5from ..utils import ensureDir
6
7
8class 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
Note: See TracBrowser for help on using the repository browser.