source: framspy/FramsticksLib.py @ 1197

Last change on this file since 1197 was 1196, checked in by Maciej Komosinski, 22 months ago

When importing *.sim files, make problems fatal because error messages are easy to overlook in output

File size: 13.2 KB
RevLine 
[1078]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:
[1196]10        """Communicates directly with Framsticks library (.dll or .so or .dylib).
[1078]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.
[1081]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.
[1078]15
[1090]16        Should you want to modify or extend this class, first see and test the examples in frams-test.py.
17
[1196]18        You need to provide one or two parameters when you run this class: the path to Framsticks where .dll/.so/.dylib resides
19        and, optionally, the name of the Framsticks dll/so/dylib (if it is non-standard). See::
[1078]20                FramsticksLib.py -h"""
21
[1084]22        PRINT_FRAMSTICKS_OUTPUT: bool = False  # set to True for debugging
[1078]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
[1149]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        ]
[1078]32
33
[1114]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
[1149]42        def __init__(self, frams_path, frams_lib_name, sim_settings_files):
[1087]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
[1078]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:
[1177]59                        frams.Math.randomize()
[1101]60                frams.Simulator.expdef = "standard-eval"  # this expdef (or fully compatible) must be used by EVALUATION_SETTINGS_FILE
[1149]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
[1196]65               
[1149]66                for simfile in self.EVALUATION_SETTINGS_FILE:
[1196]67                        ec = frams.MessageCatcher.new()  # catch potential errors, warnings, messages - just to detect if there are ERRORs
68                        ec.store = 2; # store all, because they are caught by MessageCatcher and will not appear on console (which we want)
[1149]69                        frams.Simulator.ximport(simfile, 4 + 8 + 16)
[1196]70                        ec.close()
71                        print(ec.messages) # output all caught messages
72                        assert ec.error_count._value() == 0, "Problem while importing file '%s'" % simfile # make missing files fatal because error messages are easy to overlook
[1078]73
74
75        def getSimplest(self, genetic_format) -> str:
76                return frams.GenMan.getSimplest(genetic_format).genotype._string()
77
78
79        def evaluate(self, genotype_list: List[str]):
80                """
81                Returns:
82                        List of dictionaries containing the performance of genotypes evaluated using self.EVALUATION_SETTINGS_FILE.
83                        Note that for whatever reason (e.g. incorrect genotype), the dictionaries you will get may be empty or
84                        partially empty and may not have the fields you expected, so handle such cases properly.
85                """
[1177]86                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
[1078]87
[1084]88                if not self.PRINT_FRAMSTICKS_OUTPUT:
89                        ec = frams.MessageCatcher.new()  # mute potential errors, warnings, messages
90
[1078]91                frams.GenePools[0].clear()
92                for g in genotype_list:
93                        frams.GenePools[0].add(g)
94                frams.ExpProperties.evalsavefile = ""  # no need to store results in a file - we will get evaluations directly from Genotype's "data" field
95                frams.Simulator.init()
96                frams.Simulator.start()
97
[1116]98                # step = frams.Simulator.step  # cache reference to avoid repeated lookup in the loop (just for performance)
99                # while frams.Simulator.running._int():  # standard-eval.expdef sets running to 0 when the evaluation is complete
100                #       step()
101                frams.Simulator.eval("while(Simulator.running) Simulator.step();")  # fastest
102                # Timing for evaluating a single simple creature 100x:
103                # - python step without caching: 2.2s
104                # - python step with caching   : 1.6s
105                # - pure FramScript and eval() : 0.4s
106
[1084]107                if not self.PRINT_FRAMSTICKS_OUTPUT:
108                        if ec.error_count._value() > 0:  # errors are important and should not be ignored, at least display how many
[1196]109                                print("[ERROR]", ec.error_count, "error(s) and", ec.warning_count-ec.error_count, "warning(s) while evaluating", len(genotype_list), "genotype(s)")
[1084]110                        ec.close()
111
[1078]112                results = []
113                for g in frams.GenePools[0]:
114                        serialized_dict = frams.String.serialize(g.data[frams.ExpProperties.evalsavedata._value()])
[1177]115                        evaluations = json.loads(serialized_dict._string())  # Framsticks native ExtValue's get converted to native python types such as int, float, list, str.
[1078]116                        # 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.
117                        result = {"num": g.num._value(), "name": g.name._value(), "evaluations": evaluations}
118                        results.append(result)
119
120                return results
121
122
123        def mutate(self, genotype_list: List[str]) -> List[str]:
124                """
125                Returns:
126                        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).
127                """
[1177]128                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
[1078]129
130                mutated = []
131                for g in genotype_list:
132                        mutated.append(frams.GenMan.mutate(frams.Geno.newFromString(g)).genotype._string())
133                assert len(genotype_list) == len(mutated), "Submitted %d genotypes, received %d validity values" % (len(genotype_list), len(mutated))
134                return mutated
135
136
137        def crossOver(self, genotype_parent1: str, genotype_parent2: str) -> str:
138                """
139                Returns:
140                        The genotype of the offspring. self.GENOTYPE_INVALID if the crossing over failed.
141                """
142                return frams.GenMan.crossOver(frams.Geno.newFromString(genotype_parent1), frams.Geno.newFromString(genotype_parent2)).genotype._string()
143
144
[1177]145        def dissimilarity(self, genotype_list: List[str], method: int) -> np.ndarray:
[1078]146                """
[1177]147                        :param method: -1 = genetic Levenshtein distance; 0, 1, 2 = phenetic dissimilarity (SimilMeasureGreedy, SimilMeasureHungarian, SimilMeasureDistribution)
148                        :return: A square array with dissimilarities of each pair of genotypes.
[1078]149                """
[1177]150                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
[1078]151
[1091]152                # if you want to override what EVALUATION_SETTINGS_FILE sets, you can do it below:
153                # frams.SimilMeasureHungarian.simil_partgeom = 1
154                # frams.SimilMeasureHungarian.simil_weightedMDS = 1
[1078]155
156                n = len(genotype_list)
157                square_matrix = np.zeros((n, n))
158
[1177]159                if method in (0, 1, 2):  # Framsticks phenetic dissimilarity methods
160                        frams.SimilMeasure.simil_type = method
161                        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
162                        for g in genotype_list:
163                                genos.append(frams.Geno.newFromString(g))
164                        frams_evaluateDistance = frams.SimilMeasure.evaluateDistance  # cache function reference for better performance in loops
165                        for i in range(n):
166                                for j in range(n):  # maybe calculate only one triangle if you really need a 2x speedup
167                                        square_matrix[i][j] = frams_evaluateDistance(genos[i], genos[j])._double()
168                elif method == -1:
169                        import Levenshtein
170                        for i in range(n):
171                                for j in range(n):  # maybe calculate only one triangle if you really need a 2x speedup
172                                        square_matrix[i][j] = Levenshtein.distance(genotype_list[i], genotype_list[j])
173                else:
174                        raise Exception("Don't know what to do with dissimilarity method = %d" % method)
175
[1078]176                for i in range(n):
177                        assert square_matrix[i][i] == 0, "Not a correct dissimilarity matrix, diagonal expected to be 0"
[1102]178                non_symmetric_diff = square_matrix - square_matrix.T
179                non_symmetric_count = np.count_nonzero(non_symmetric_diff)
180                if non_symmetric_count > 0:
181                        non_symmetric_diff_abs = np.abs(non_symmetric_diff)
[1177]182                        max_pos1d = np.argmax(non_symmetric_diff_abs)  # location of the largest discrepancy
183                        max_pos2d_XY = np.unravel_index(max_pos1d, non_symmetric_diff_abs.shape)  # 2D coordinates of the largest discrepancy
184                        max_pos2d_YX = max_pos2d_XY[1], max_pos2d_XY[0]  # 2D coordinates of the largest discrepancy mirror
[1102]185                        worst_guy_XY = square_matrix[max_pos2d_XY]  # this distance and the other below (its mirror) are most different
186                        worst_guy_YX = square_matrix[max_pos2d_YX]
187                        print("[WARN] Dissimilarity matrix: expecting symmetry, but %g out of %d pairs were asymmetrical, max difference was %g (%g %%)" %
188                              (non_symmetric_count / 2,
189                               n * (n - 1) / 2,
190                               non_symmetric_diff_abs[max_pos2d_XY],
191                               non_symmetric_diff_abs[max_pos2d_XY] * 100 / ((worst_guy_XY + worst_guy_YX) / 2)))  # max diff is not necessarily max %
[1078]192                return square_matrix
193
194
195        def isValid(self, genotype_list: List[str]) -> List[bool]:
[1177]196                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
[1078]197                valid = []
198                for g in genotype_list:
199                        valid.append(frams.Geno.newFromString(g).is_valid._int() == 1)
[1102]200                assert len(genotype_list) == len(valid), "Tested %d genotypes, received %d validity values" % (len(genotype_list), len(valid))
[1078]201                return valid
202
203
204def parseArguments():
205        parser = argparse.ArgumentParser(description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[0])
[1196]206        parser.add_argument('-path', type=ensureDir, required=True, help='Path to the Framsticks library (.dll or .so or .dylib) without trailing slash.')
207        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.')
[1091]208        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.')
[1078]209        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.')
210        return parser.parse_args()
211
212
213def ensureDir(string):
214        if os.path.isdir(string):
215                return string
216        else:
217                raise NotADirectoryError(string)
218
219
220if __name__ == "__main__":
221        # A demo run.
222
223        # TODO ideas:
224        # - check_validity with three levels (invalid, corrected, valid)
225        # - a pool of binaries running simultaneously, balance load - in particular evaluation
226
227        parsed_args = parseArguments()
[1102]228        framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.simsettings)
[1078]229
[1101]230        print("Sending a direct command to Framsticks library that calculates \"4\"+2 yields", frams.Simulator.eval("return \"4\"+2;"))
[1078]231
[1102]232        simplest = framsLib.getSimplest('1' if parsed_args.genformat is None else parsed_args.genformat)
[1078]233        print("\tSimplest genotype:", simplest)
[1102]234        parent1 = framsLib.mutate([simplest])[0]
[1078]235        parent2 = parent1
236        MUTATE_COUNT = 10
237        for x in range(MUTATE_COUNT):  # example of a chain of 10 mutations
[1102]238                parent2 = framsLib.mutate([parent2])[0]
[1078]239        print("\tParent1 (mutated simplest):", parent1)
240        print("\tParent2 (Parent1 mutated %d times):" % MUTATE_COUNT, parent2)
[1102]241        offspring = framsLib.crossOver(parent1, parent2)
[1078]242        print("\tCrossover (Offspring):", offspring)
[1177]243        print('\tDissimilarity of Parent1 and Offspring:', framsLib.dissimilarity([parent1, offspring], 1)[0, 1])
[1102]244        print('\tPerformance of Offspring:', framsLib.evaluate([offspring]))
245        print('\tValidity of Parent1, Parent 2, and Offspring:', framsLib.isValid([parent1, parent2, offspring]))
Note: See TracBrowser for help on using the repository browser.