source: framspy/FramsticksLib.py @ 1078

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

Direct connection from python to Framsticks .so/.dll

File size: 9.3 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 DLL/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
15        You need to provide one or two parameters when you run this class: the path to Framsticks CLI where .dll/.so resides
16        and the name of the Framsticks dll/so (if it is non-standard). See::
17                FramsticksLib.py -h"""
18
19        DETERMINISTIC: bool = False  # set to True to have the same results in each run
20
21        GENOTYPE_INVALID = "/*invalid*/"  # this is how genotype invalidity is represented in Framsticks
22        EVALUATION_SETTINGS_FILE = "eval-allcriteria.sim"  # MUST be compatible with standard-eval expdef
23
24
25        def __init__(self, framspath, framsexe, pid=""):
26                self.pid = pid if pid is not None else ""
27                self.frams_path = framspath
28                self.frams_exe = framsexe if framsexe is not None else 'frams.exe' if os.name == "nt" else 'frams.linux'
29                self.writing_path = None
30                mainpath = os.path.join(self.frams_path, self.frams_exe)
31                # exe_call = [mainpath, '-Q', '-s', '-c', '-icliutils.ini']  # -c will be ignored in Windows Framsticks (this option is meaningless because the Windows version does not support color console, so no need to deactivate this feature using -c)
32                # exe_call_to_get_version = [mainpath, '-V']
33                # exe_call_to_get_path = [mainpath, '-?']
34
35                frams.init(framspath, "-Ddata")  # "-D"+os.path.join(framspath,"data")) # not possible (maybe python windows issue) because of the need for os.chdir(). So we assume "data" is where the dll/so is
36
37                print('Available objects:', dir(frams))
38                print()
39
40                self.__spawnFramsticksCLI()
41
42
43        def __spawnFramsticksCLI(self):
44                print('Performing a basic test 1/2... ', end='')
45                simplest = self.getSimplest("1")
46                assert simplest == "X" and type(simplest) is str
47                print('OK.')
48                print('Performing a basic test 2/2... ', end='')
49                assert self.isValid(["X[0:0],", "X[0:0]", "X[1:0]"]) == [False, True, False]
50                print('OK.')
51                if not self.DETERMINISTIC:
52                        frams.Math.randomize();
53                frams.Simulator.expdef = "standard-eval"  # this expdef must be used by EVALUATION_SETTINGS_FILE
54
55
56        def getSimplest(self, genetic_format) -> str:
57                return frams.GenMan.getSimplest(genetic_format).genotype._string()
58
59
60        def evaluate(self, genotype_list: List[str]):
61                """
62                Returns:
63                        List of dictionaries containing the performance of genotypes evaluated using self.EVALUATION_SETTINGS_FILE.
64                        Note that for whatever reason (e.g. incorrect genotype), the dictionaries you will get may be empty or
65                        partially empty and may not have the fields you expected, so handle such cases properly.
66                """
67                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
68
69                # TODO use Logger to mute stdout (optinally just like in FramsticksCLI.py)
70                frams.GenePools[0].clear()
71                frams.Simulator.ximport(self.EVALUATION_SETTINGS_FILE, 2 + 4 + 8 + 16)
72                for g in genotype_list:
73                        frams.GenePools[0].add(g)
74                frams.ExpProperties.evalsavefile = ""  # no need to store results in a file - we will get evaluations directly from Genotype's "data" field
75                frams.Simulator.init()
76                frams.Simulator.start()
77                step = frams.Simulator.step  # cache reference to avoid repeated lookup in the loop
78                while frams.Simulator.running._int():  # standard-eval.expdef sets running to 0 when the evaluation is complete
79                        step()
80
81                results = []
82                for g in frams.GenePools[0]:
83                        serialized_dict = frams.String.serialize(g.data[frams.ExpProperties.evalsavedata._value()])
84                        evaluations = json.loads(serialized_dict._string())
85                        # 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.
86                        result = {"num": g.num._value(), "name": g.name._value(), "evaluations": evaluations}
87                        results.append(result)
88
89                return results
90
91
92        def mutate(self, genotype_list: List[str]) -> List[str]:
93                """
94                Returns:
95                        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).
96                """
97                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
98
99                mutated = []
100                for g in genotype_list:
101                        mutated.append(frams.GenMan.mutate(frams.Geno.newFromString(g)).genotype._string())
102                assert len(genotype_list) == len(mutated), "Submitted %d genotypes, received %d validity values" % (len(genotype_list), len(mutated))
103                return mutated
104
105
106        def crossOver(self, genotype_parent1: str, genotype_parent2: str) -> str:
107                """
108                Returns:
109                        The genotype of the offspring. self.GENOTYPE_INVALID if the crossing over failed.
110                """
111                return frams.GenMan.crossOver(frams.Geno.newFromString(genotype_parent1), frams.Geno.newFromString(genotype_parent2)).genotype._string()
112
113
114        def dissimilarity(self, genotype_list: List[str]) -> np.ndarray:
115                """
116                Returns:
117                        A square array with dissimilarities of each pair of genotypes.
118                """
119                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
120
121                frams.SimilMeasure.type = 1  # adjust to your needs. Set here because loading EVALUATION_SETTINGS_FILE during evaluation may overwrite these parameters
122                frams.SimilMeasureHungarian.simil_weightedMDS = 1
123                frams.SimilMeasureHungarian.simil_partgeom = 1
124
125                n = len(genotype_list)
126                square_matrix = np.zeros((n, n))
127                genos = []  # prepare an array of Geno objects so we don't need to convert raw strings to Geno objects all the time
128                for g in genotype_list:
129                        genos.append(frams.Geno.newFromString(g))
130                for i in range(n):
131                        for j in range(n):  # maybe calculate only one triangle if you really need a 2x speedup
132                                square_matrix[i][j] = frams.SimilMeasure.evaluateDistance(genos[i], genos[j])._double()
133
134                for i in range(n):
135                        assert square_matrix[i][i] == 0, "Not a correct dissimilarity matrix, diagonal expected to be 0"
136                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
137                return square_matrix
138
139
140        def isValid(self, genotype_list: List[str]) -> List[bool]:
141                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
142                valid = []
143                for g in genotype_list:
144                        valid.append(frams.Geno.newFromString(g).is_valid._int() == 1)
145                assert len(genotype_list) == len(valid), "Submitted %d genotypes, received %d validity values" % (len(genotype_list), len(valid))
146                return valid
147
148
149def parseArguments():
150        parser = argparse.ArgumentParser(description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[0])
151        parser.add_argument('-path', type=ensureDir, required=True, help='Path to Framsticks CLI without trailing slash.')
152        parser.add_argument('-exe', required=False, help='Library name. If not given, "frams.dll" or "frams.so" is assumed depending on the platform.')  # TODO dll,so, allow to change name?
153        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.')
154        return parser.parse_args()
155
156
157def ensureDir(string):
158        if os.path.isdir(string):
159                return string
160        else:
161                raise NotADirectoryError(string)
162
163
164if __name__ == "__main__":
165        # A demo run.
166
167        # TODO ideas:
168        # - check_validity with three levels (invalid, corrected, valid)
169        # - a pool of binaries running simultaneously, balance load - in particular evaluation
170
171        parsed_args = parseArguments()
172        framsDLL = FramsticksLib(parsed_args.path, parsed_args.exe, None)  # parsed_args.pid)
173
174        print("Sending a direct command to Framsticks CLI that calculates \"4\"+2 yields", frams.Simulator.eval("return \"4\"+2;"))
175
176        simplest = framsDLL.getSimplest('1' if parsed_args.genformat is None else parsed_args.genformat)
177        print("\tSimplest genotype:", simplest)
178        parent1 = framsDLL.mutate([simplest])[0]
179        parent2 = parent1
180        MUTATE_COUNT = 10
181        for x in range(MUTATE_COUNT):  # example of a chain of 10 mutations
182                parent2 = framsDLL.mutate([parent2])[0]
183        print("\tParent1 (mutated simplest):", parent1)
184        print("\tParent2 (Parent1 mutated %d times):" % MUTATE_COUNT, parent2)
185        offspring = framsDLL.crossOver(parent1, parent2)
186        print("\tCrossover (Offspring):", offspring)
187        print('\tDissimilarity of Parent1 and Offspring:', framsDLL.dissimilarity([parent1, offspring])[0, 1])
188        print('\tPerformance of Offspring:', framsDLL.evaluate([offspring]))
189        print('\tValidity of Parent1, Parent 2, and Offspring:', framsDLL.isValid([parent1, parent2, offspring]))
Note: See TracBrowser for help on using the repository browser.