source: framspy/FramsticksLib.py @ 1139

Last change on this file since 1139 was 1119, checked in by Maciej Komosinski, 4 years ago

Cache a function reference for better performance

File size: 11.3 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:
[1087]10        """Communicates directly with Framsticks library (.dll or .so).
[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
[1087]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::
[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
[1091]26        EVALUATION_SETTINGS_FILE = "eval-allcriteria.sim"  # MUST be compatible with the standard-eval expdef
[1078]27
28
[1114]29        # This function is not needed because in python, "For efficiency reasons, each module is only imported once per interpreter session."
30        # @staticmethod
31        # def getFramsModuleInstance():
32        #       """If some other party needs access to the frams module to directly access or modify Framsticks objects,
33        #       use this function to avoid importing the "frams" module multiple times and avoid potentially initializing
34        #       it many times."""
35        #       return frams
36
[1091]37        def __init__(self, frams_path, frams_lib_name, simsettings):
[1087]38                if frams_lib_name is None:
39                        frams.init(frams_path)  # could add support for setting alternative directories using -D and -d
40                else:
41                        frams.init(frams_path, "-L" + frams_lib_name)  # could add support for setting alternative directories using -D and -d
[1078]42
43                print('Available objects:', dir(frams))
44                print()
45
46                print('Performing a basic test 1/2... ', end='')
47                simplest = self.getSimplest("1")
48                assert simplest == "X" and type(simplest) is str
49                print('OK.')
50                print('Performing a basic test 2/2... ', end='')
51                assert self.isValid(["X[0:0],", "X[0:0]", "X[1:0]"]) == [False, True, False]
52                print('OK.')
53                if not self.DETERMINISTIC:
54                        frams.Math.randomize();
[1101]55                frams.Simulator.expdef = "standard-eval"  # this expdef (or fully compatible) must be used by EVALUATION_SETTINGS_FILE
[1091]56                if simsettings is not None:
57                        self.EVALUATION_SETTINGS_FILE = simsettings
[1101]58                frams.Simulator.ximport(self.EVALUATION_SETTINGS_FILE, 4 + 8 + 16)
[1078]59
60
61        def getSimplest(self, genetic_format) -> str:
62                return frams.GenMan.getSimplest(genetic_format).genotype._string()
63
64
65        def evaluate(self, genotype_list: List[str]):
66                """
67                Returns:
68                        List of dictionaries containing the performance of genotypes evaluated using self.EVALUATION_SETTINGS_FILE.
69                        Note that for whatever reason (e.g. incorrect genotype), the dictionaries you will get may be empty or
70                        partially empty and may not have the fields you expected, so handle such cases properly.
71                """
72                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
73
[1084]74                if not self.PRINT_FRAMSTICKS_OUTPUT:
75                        ec = frams.MessageCatcher.new()  # mute potential errors, warnings, messages
76
[1078]77                frams.GenePools[0].clear()
78                for g in genotype_list:
79                        frams.GenePools[0].add(g)
80                frams.ExpProperties.evalsavefile = ""  # no need to store results in a file - we will get evaluations directly from Genotype's "data" field
81                frams.Simulator.init()
82                frams.Simulator.start()
83
[1116]84                # step = frams.Simulator.step  # cache reference to avoid repeated lookup in the loop (just for performance)
85                # while frams.Simulator.running._int():  # standard-eval.expdef sets running to 0 when the evaluation is complete
86                #       step()
87                frams.Simulator.eval("while(Simulator.running) Simulator.step();")  # fastest
88                # Timing for evaluating a single simple creature 100x:
89                # - python step without caching: 2.2s
90                # - python step with caching   : 1.6s
91                # - pure FramScript and eval() : 0.4s
92
[1084]93                if not self.PRINT_FRAMSTICKS_OUTPUT:
94                        if ec.error_count._value() > 0:  # errors are important and should not be ignored, at least display how many
95                                print("[ERROR]", ec.error_count, "error(s) and", ec.warning_count, "warning(s) while evaluating", len(genotype_list), "genotype(s)")
96                        ec.close()
97
[1078]98                results = []
99                for g in frams.GenePools[0]:
100                        serialized_dict = frams.String.serialize(g.data[frams.ExpProperties.evalsavedata._value()])
101                        evaluations = json.loads(serialized_dict._string())
102                        # 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.
103                        result = {"num": g.num._value(), "name": g.name._value(), "evaluations": evaluations}
104                        results.append(result)
105
106                return results
107
108
109        def mutate(self, genotype_list: List[str]) -> List[str]:
110                """
111                Returns:
112                        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).
113                """
114                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
115
116                mutated = []
117                for g in genotype_list:
118                        mutated.append(frams.GenMan.mutate(frams.Geno.newFromString(g)).genotype._string())
119                assert len(genotype_list) == len(mutated), "Submitted %d genotypes, received %d validity values" % (len(genotype_list), len(mutated))
120                return mutated
121
122
123        def crossOver(self, genotype_parent1: str, genotype_parent2: str) -> str:
124                """
125                Returns:
126                        The genotype of the offspring. self.GENOTYPE_INVALID if the crossing over failed.
127                """
128                return frams.GenMan.crossOver(frams.Geno.newFromString(genotype_parent1), frams.Geno.newFromString(genotype_parent2)).genotype._string()
129
130
131        def dissimilarity(self, genotype_list: List[str]) -> np.ndarray:
132                """
133                Returns:
134                        A square array with dissimilarities of each pair of genotypes.
135                """
136                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
137
[1091]138                # if you want to override what EVALUATION_SETTINGS_FILE sets, you can do it below:
[1095]139                # frams.SimilMeasure.simil_type = 1
[1091]140                # frams.SimilMeasureHungarian.simil_partgeom = 1
141                # frams.SimilMeasureHungarian.simil_weightedMDS = 1
[1078]142
143                n = len(genotype_list)
144                square_matrix = np.zeros((n, n))
[1119]145                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
[1078]146                for g in genotype_list:
147                        genos.append(frams.Geno.newFromString(g))
[1119]148                frams_evaluateDistance = frams.SimilMeasure.evaluateDistance  # cache function reference for better performance in loops
[1078]149                for i in range(n):
150                        for j in range(n):  # maybe calculate only one triangle if you really need a 2x speedup
[1119]151                                square_matrix[i][j] = frams_evaluateDistance(genos[i], genos[j])._double()
[1078]152
153                for i in range(n):
154                        assert square_matrix[i][i] == 0, "Not a correct dissimilarity matrix, diagonal expected to be 0"
[1102]155                non_symmetric_diff = square_matrix - square_matrix.T
156                non_symmetric_count = np.count_nonzero(non_symmetric_diff)
157                if non_symmetric_count > 0:
158                        non_symmetric_diff_abs = np.abs(non_symmetric_diff)
159                        max_pos1d = np.argmax(non_symmetric_diff_abs)  # location of largest discrepancy
160                        max_pos2d_XY = np.unravel_index(max_pos1d, non_symmetric_diff_abs.shape)  # 2D coordinates of largest discrepancy
161                        max_pos2d_YX = max_pos2d_XY[1], max_pos2d_XY[0]  # 2D coordinates of largest discrepancy mirror
162                        worst_guy_XY = square_matrix[max_pos2d_XY]  # this distance and the other below (its mirror) are most different
163                        worst_guy_YX = square_matrix[max_pos2d_YX]
164                        print("[WARN] Dissimilarity matrix: expecting symmetry, but %g out of %d pairs were asymmetrical, max difference was %g (%g %%)" %
165                              (non_symmetric_count / 2,
166                               n * (n - 1) / 2,
167                               non_symmetric_diff_abs[max_pos2d_XY],
168                               non_symmetric_diff_abs[max_pos2d_XY] * 100 / ((worst_guy_XY + worst_guy_YX) / 2)))  # max diff is not necessarily max %
[1078]169                return square_matrix
170
171
172        def isValid(self, genotype_list: List[str]) -> List[bool]:
173                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
174                valid = []
175                for g in genotype_list:
176                        valid.append(frams.Geno.newFromString(g).is_valid._int() == 1)
[1102]177                assert len(genotype_list) == len(valid), "Tested %d genotypes, received %d validity values" % (len(genotype_list), len(valid))
[1078]178                return valid
179
180
181def parseArguments():
182        parser = argparse.ArgumentParser(description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[0])
[1084]183        parser.add_argument('-path', type=ensureDir, required=True, help='Path to the Framsticks library (.dll or .so) without trailing slash.')
184        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.')
[1091]185        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]186        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.')
187        return parser.parse_args()
188
189
190def ensureDir(string):
191        if os.path.isdir(string):
192                return string
193        else:
194                raise NotADirectoryError(string)
195
196
197if __name__ == "__main__":
198        # A demo run.
199
200        # TODO ideas:
201        # - check_validity with three levels (invalid, corrected, valid)
202        # - a pool of binaries running simultaneously, balance load - in particular evaluation
203
204        parsed_args = parseArguments()
[1102]205        framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.simsettings)
[1078]206
[1101]207        print("Sending a direct command to Framsticks library that calculates \"4\"+2 yields", frams.Simulator.eval("return \"4\"+2;"))
[1078]208
[1102]209        simplest = framsLib.getSimplest('1' if parsed_args.genformat is None else parsed_args.genformat)
[1078]210        print("\tSimplest genotype:", simplest)
[1102]211        parent1 = framsLib.mutate([simplest])[0]
[1078]212        parent2 = parent1
213        MUTATE_COUNT = 10
214        for x in range(MUTATE_COUNT):  # example of a chain of 10 mutations
[1102]215                parent2 = framsLib.mutate([parent2])[0]
[1078]216        print("\tParent1 (mutated simplest):", parent1)
217        print("\tParent2 (Parent1 mutated %d times):" % MUTATE_COUNT, parent2)
[1102]218        offspring = framsLib.crossOver(parent1, parent2)
[1078]219        print("\tCrossover (Offspring):", offspring)
[1102]220        print('\tDissimilarity of Parent1 and Offspring:', framsLib.dissimilarity([parent1, offspring])[0, 1])
221        print('\tPerformance of Offspring:', framsLib.evaluate([offspring]))
222        print('\tValidity of Parent1, Parent 2, and Offspring:', framsLib.isValid([parent1, parent2, offspring]))
Note: See TracBrowser for help on using the repository browser.