1 | from typing import List # to be able to specify a type hint of list(something)
|
---|
2 | import json
|
---|
3 | import sys, os
|
---|
4 | import argparse
|
---|
5 | import numpy as np
|
---|
6 | import frams
|
---|
7 |
|
---|
8 |
|
---|
9 | class 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 |
|
---|
149 | def 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 |
|
---|
157 | def ensureDir(string):
|
---|
158 | if os.path.isdir(string):
|
---|
159 | return string
|
---|
160 | else:
|
---|
161 | raise NotADirectoryError(string)
|
---|
162 |
|
---|
163 |
|
---|
164 | if __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]))
|
---|