source: framspy/FramsticksLib.py @ 1092

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

Allowed to override settings of the simulation using a file with the file name provided as an argument

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