source: framspy/FramsticksLib.py @ 1149

Last change on this file since 1149 was 1149, checked in by Maciej Komosinski, 2 years ago

Added support for loading multiple .sim files where each can overwrite selected settings

File size: 12.0 KB
Line 
1from typing import List  # to be able to specify a type hint of list(something)
2import json
3import sys, os
4import argparse
5import numpy as np
6import frams
7
8
9class FramsticksLib:
10        """Communicates directly with Framsticks library (.dll or .so).
11        You can perform basic operations like mutation, crossover, and evaluation of genotypes.
12        This way you can perform evolution controlled by python as well as access and manipulate genotypes.
13        You can even design and use in evolution your own genetic representation implemented entirely in python,
14        or access and control the simulation and simulated creatures step by step.
15
16        Should you want to modify or extend this class, first see and test the examples in frams-test.py.
17
18        You need to provide one or two parameters when you run this class: the path to Framsticks where .dll/.so resides
19        and, optionally, the name of the Framsticks dll/so (if it is non-standard). See::
20                FramsticksLib.py -h"""
21
22        PRINT_FRAMSTICKS_OUTPUT: bool = False  # set to True for debugging
23        DETERMINISTIC: bool = False  # set to True to have the same results in each run
24
25        GENOTYPE_INVALID = "/*invalid*/"  # this is how genotype invalidity is represented in Framsticks
26        EVALUATION_SETTINGS_FILE = [  # all files MUST be compatible with the standard-eval expdef. The order they are loaded in is important!
27                "eval-allcriteria.sim",  # a good trade-off in performance sampling period ("perfperiod") for vertpos and velocity
28                # "deterministic.sim",  # turns off random noise (added for robustness) so that each evaluation yields identical performance values (causes "overfitting")
29                # "sample-period-2.sim", # short performance sampling period so performance (e.g. vertical position) is sampled more often
30                # "sample-period-longest.sim",  # increased performance sampling period so distance and velocity are measured rectilinearly
31        ]
32
33
34        # This function is not needed because in python, "For efficiency reasons, each module is only imported once per interpreter session."
35        # @staticmethod
36        # def getFramsModuleInstance():
37        #       """If some other party needs access to the frams module to directly access or modify Framsticks objects,
38        #       use this function to avoid importing the "frams" module multiple times and avoid potentially initializing
39        #       it many times."""
40        #       return frams
41
42        def __init__(self, frams_path, frams_lib_name, sim_settings_files):
43                if frams_lib_name is None:
44                        frams.init(frams_path)  # could add support for setting alternative directories using -D and -d
45                else:
46                        frams.init(frams_path, "-L" + frams_lib_name)  # could add support for setting alternative directories using -D and -d
47
48                print('Available objects:', dir(frams))
49                print()
50
51                print('Performing a basic test 1/2... ', end='')
52                simplest = self.getSimplest("1")
53                assert simplest == "X" and type(simplest) is str
54                print('OK.')
55                print('Performing a basic test 2/2... ', end='')
56                assert self.isValid(["X[0:0],", "X[0:0]", "X[1:0]"]) == [False, True, False]
57                print('OK.')
58                if not self.DETERMINISTIC:
59                        frams.Math.randomize();
60                frams.Simulator.expdef = "standard-eval"  # this expdef (or fully compatible) must be used by EVALUATION_SETTINGS_FILE
61                if sim_settings_files is not None:
62                        self.EVALUATION_SETTINGS_FILE = sim_settings_files
63                print('Using settings:', self.EVALUATION_SETTINGS_FILE)
64                assert isinstance(self.EVALUATION_SETTINGS_FILE, list)  # ensure settings file(s) are provided as a list
65                for simfile in self.EVALUATION_SETTINGS_FILE:
66                        frams.Simulator.ximport(simfile, 4 + 8 + 16)
67
68
69        def getSimplest(self, genetic_format) -> str:
70                return frams.GenMan.getSimplest(genetic_format).genotype._string()
71
72
73        def evaluate(self, genotype_list: List[str]):
74                """
75                Returns:
76                        List of dictionaries containing the performance of genotypes evaluated using self.EVALUATION_SETTINGS_FILE.
77                        Note that for whatever reason (e.g. incorrect genotype), the dictionaries you will get may be empty or
78                        partially empty and may not have the fields you expected, so handle such cases properly.
79                """
80                assert isinstance(genotype_list, list)  # because in python str has similar capabilities as list and here it would pretend to work too, so to avoid any ambiguity
81
82                if not self.PRINT_FRAMSTICKS_OUTPUT:
83                        ec = frams.MessageCatcher.new()  # mute potential errors, warnings, messages
84
85                frams.GenePools[0].clear()
86                for g in genotype_list:
87                        frams.GenePools[0].add(g)
88                frams.ExpProperties.evalsavefile = ""  # no need to store results in a file - we will get evaluations directly from Genotype's "data" field
89                frams.Simulator.init()
90                frams.Simulator.start()
91
92                # step = frams.Simulator.step  # cache reference to avoid repeated lookup in the loop (just for performance)
93                # while frams.Simulator.running._int():  # standard-eval.expdef sets running to 0 when the evaluation is complete
94                #       step()
95                frams.Simulator.eval("while(Simulator.running) Simulator.step();")  # fastest
96                # Timing for evaluating a single simple creature 100x:
97                # - python step without caching: 2.2s
98                # - python step with caching   : 1.6s
99                # - pure FramScript and eval() : 0.4s
100
101                if not self.PRINT_FRAMSTICKS_OUTPUT:
102                        if ec.error_count._value() > 0:  # errors are important and should not be ignored, at least display how many
103                                print("[ERROR]", ec.error_count, "error(s) and", ec.warning_count, "warning(s) while evaluating", len(genotype_list), "genotype(s)")
104                        ec.close()
105
106                results = []
107                for g in frams.GenePools[0]:
108                        serialized_dict = frams.String.serialize(g.data[frams.ExpProperties.evalsavedata._value()])
109                        evaluations = json.loads(serialized_dict._string())
110                        # now, for consistency with FramsticksCLI.py, add "num" and "name" keys that are missing because we got data directly from Genotype, not from the file produced by standard-eval.expdef's function printStats(). What we do below is what printStats() does.
111                        result = {"num": g.num._value(), "name": g.name._value(), "evaluations": evaluations}
112                        results.append(result)
113
114                return results
115
116
117        def mutate(self, genotype_list: List[str]) -> List[str]:
118                """
119                Returns:
120                        The genotype(s) of the mutated source genotype(s). self.GENOTYPE_INVALID for genotypes whose mutation failed (for example because the source genotype was invalid).
121                """
122                assert isinstance(genotype_list, list)  # because in python str has similar capabilities as list and here it would pretend to work too, so to avoid any ambiguity
123
124                mutated = []
125                for g in genotype_list:
126                        mutated.append(frams.GenMan.mutate(frams.Geno.newFromString(g)).genotype._string())
127                assert len(genotype_list) == len(mutated), "Submitted %d genotypes, received %d validity values" % (len(genotype_list), len(mutated))
128                return mutated
129
130
131        def crossOver(self, genotype_parent1: str, genotype_parent2: str) -> str:
132                """
133                Returns:
134                        The genotype of the offspring. self.GENOTYPE_INVALID if the crossing over failed.
135                """
136                return frams.GenMan.crossOver(frams.Geno.newFromString(genotype_parent1), frams.Geno.newFromString(genotype_parent2)).genotype._string()
137
138
139        def dissimilarity(self, genotype_list: List[str]) -> np.ndarray:
140                """
141                Returns:
142                        A square array with dissimilarities of each pair of genotypes.
143                """
144                assert isinstance(genotype_list, list)  # because in python str has similar capabilities as list and here it would pretend to work too, so to avoid any ambiguity
145
146                # if you want to override what EVALUATION_SETTINGS_FILE sets, you can do it below:
147                # frams.SimilMeasure.simil_type = 1
148                # frams.SimilMeasureHungarian.simil_partgeom = 1
149                # frams.SimilMeasureHungarian.simil_weightedMDS = 1
150
151                n = len(genotype_list)
152                square_matrix = np.zeros((n, n))
153                genos = []  # prepare an array of Geno objects so that we don't need to convert raw strings to Geno objects all the time in loops
154                for g in genotype_list:
155                        genos.append(frams.Geno.newFromString(g))
156                frams_evaluateDistance = frams.SimilMeasure.evaluateDistance  # cache function reference for better performance in loops
157                for i in range(n):
158                        for j in range(n):  # maybe calculate only one triangle if you really need a 2x speedup
159                                square_matrix[i][j] = frams_evaluateDistance(genos[i], genos[j])._double()
160
161                for i in range(n):
162                        assert square_matrix[i][i] == 0, "Not a correct dissimilarity matrix, diagonal expected to be 0"
163                non_symmetric_diff = square_matrix - square_matrix.T
164                non_symmetric_count = np.count_nonzero(non_symmetric_diff)
165                if non_symmetric_count > 0:
166                        non_symmetric_diff_abs = np.abs(non_symmetric_diff)
167                        max_pos1d = np.argmax(non_symmetric_diff_abs)  # location of largest discrepancy
168                        max_pos2d_XY = np.unravel_index(max_pos1d, non_symmetric_diff_abs.shape)  # 2D coordinates of largest discrepancy
169                        max_pos2d_YX = max_pos2d_XY[1], max_pos2d_XY[0]  # 2D coordinates of largest discrepancy mirror
170                        worst_guy_XY = square_matrix[max_pos2d_XY]  # this distance and the other below (its mirror) are most different
171                        worst_guy_YX = square_matrix[max_pos2d_YX]
172                        print("[WARN] Dissimilarity matrix: expecting symmetry, but %g out of %d pairs were asymmetrical, max difference was %g (%g %%)" %
173                              (non_symmetric_count / 2,
174                               n * (n - 1) / 2,
175                               non_symmetric_diff_abs[max_pos2d_XY],
176                               non_symmetric_diff_abs[max_pos2d_XY] * 100 / ((worst_guy_XY + worst_guy_YX) / 2)))  # max diff is not necessarily max %
177                return square_matrix
178
179
180        def isValid(self, genotype_list: List[str]) -> List[bool]:
181                assert isinstance(genotype_list, list)  # because in python str has similar capabilities as list and here it would pretend to work too, so to avoid any ambiguity
182                valid = []
183                for g in genotype_list:
184                        valid.append(frams.Geno.newFromString(g).is_valid._int() == 1)
185                assert len(genotype_list) == len(valid), "Tested %d genotypes, received %d validity values" % (len(genotype_list), len(valid))
186                return valid
187
188
189def parseArguments():
190        parser = argparse.ArgumentParser(description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[0])
191        parser.add_argument('-path', type=ensureDir, required=True, help='Path to the Framsticks library (.dll or .so) without trailing slash.')
192        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.')
193        parser.add_argument('-simsettings', required=False, 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.')
194        parser.add_argument('-genformat', required=False, help='Genetic format for the demo run, for example 4, 9, or S. If not given, f1 is assumed.')
195        return parser.parse_args()
196
197
198def ensureDir(string):
199        if os.path.isdir(string):
200                return string
201        else:
202                raise NotADirectoryError(string)
203
204
205if __name__ == "__main__":
206        # A demo run.
207
208        # TODO ideas:
209        # - check_validity with three levels (invalid, corrected, valid)
210        # - a pool of binaries running simultaneously, balance load - in particular evaluation
211
212        parsed_args = parseArguments()
213        framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.simsettings)
214
215        print("Sending a direct command to Framsticks library that calculates \"4\"+2 yields", frams.Simulator.eval("return \"4\"+2;"))
216
217        simplest = framsLib.getSimplest('1' if parsed_args.genformat is None else parsed_args.genformat)
218        print("\tSimplest genotype:", simplest)
219        parent1 = framsLib.mutate([simplest])[0]
220        parent2 = parent1
221        MUTATE_COUNT = 10
222        for x in range(MUTATE_COUNT):  # example of a chain of 10 mutations
223                parent2 = framsLib.mutate([parent2])[0]
224        print("\tParent1 (mutated simplest):", parent1)
225        print("\tParent2 (Parent1 mutated %d times):" % MUTATE_COUNT, parent2)
226        offspring = framsLib.crossOver(parent1, parent2)
227        print("\tCrossover (Offspring):", offspring)
228        print('\tDissimilarity of Parent1 and Offspring:', framsLib.dissimilarity([parent1, offspring])[0, 1])
229        print('\tPerformance of Offspring:', framsLib.evaluate([offspring]))
230        print('\tValidity of Parent1, Parent 2, and Offspring:', framsLib.isValid([parent1, parent2, offspring]))
Note: See TracBrowser for help on using the repository browser.