source: framspy/FramsticksLib.py @ 1087

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

Cosmetic

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