source: framspy/FramsticksLib.py @ 1084

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

Introduced bool PRINT_FRAMSTICKS_OUTPUT to mute messages while evaluating creatures

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