source: framspy/FramsticksLib.py @ 1090

Last change on this file since 1090 was 1090, checked in by Maciej Komosinski, 8 days ago

Cosmetic

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