source: framspy/FramsticksLib.py @ 1113

Last change on this file since 1113 was 1102, checked in by Maciej Komosinski, 3 years ago

Do not fail when the dissimilarity matrix is not symmetric, print a warning message instead

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