[1078] | 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:
|
---|
[1196] | 10 | """Communicates directly with Framsticks library (.dll or .so or .dylib).
|
---|
[1078] | 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.
|
---|
[1081] | 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.
|
---|
[1078] | 15 |
|
---|
[1090] | 16 | Should you want to modify or extend this class, first see and test the examples in frams-test.py.
|
---|
| 17 |
|
---|
[1196] | 18 | You need to provide one or two parameters when you run this class: the path to Framsticks where .dll/.so/.dylib resides
|
---|
| 19 | and, optionally, the name of the Framsticks dll/so/dylib (if it is non-standard). See::
|
---|
[1078] | 20 | FramsticksLib.py -h"""
|
---|
| 21 |
|
---|
[1084] | 22 | PRINT_FRAMSTICKS_OUTPUT: bool = False # set to True for debugging
|
---|
[1078] | 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
|
---|
[1149] | 26 | EVALUATION_SETTINGS_FILE = [ # all files MUST be compatible with the standard-eval expdef. The order they are loaded in is important!
|
---|
| 27 | "eval-allcriteria.sim", # a good trade-off in performance sampling period ("perfperiod") for vertpos and velocity
|
---|
| 28 | # "deterministic.sim", # turns off random noise (added for robustness) so that each evaluation yields identical performance values (causes "overfitting")
|
---|
| 29 | # "sample-period-2.sim", # short performance sampling period so performance (e.g. vertical position) is sampled more often
|
---|
| 30 | # "sample-period-longest.sim", # increased performance sampling period so distance and velocity are measured rectilinearly
|
---|
| 31 | ]
|
---|
[1078] | 32 |
|
---|
| 33 |
|
---|
[1114] | 34 | # This function is not needed because in python, "For efficiency reasons, each module is only imported once per interpreter session."
|
---|
| 35 | # @staticmethod
|
---|
| 36 | # def getFramsModuleInstance():
|
---|
| 37 | # """If some other party needs access to the frams module to directly access or modify Framsticks objects,
|
---|
| 38 | # use this function to avoid importing the "frams" module multiple times and avoid potentially initializing
|
---|
| 39 | # it many times."""
|
---|
| 40 | # return frams
|
---|
| 41 |
|
---|
[1149] | 42 | def __init__(self, frams_path, frams_lib_name, sim_settings_files):
|
---|
[1087] | 43 | if frams_lib_name is None:
|
---|
| 44 | frams.init(frams_path) # could add support for setting alternative directories using -D and -d
|
---|
| 45 | else:
|
---|
| 46 | frams.init(frams_path, "-L" + frams_lib_name) # could add support for setting alternative directories using -D and -d
|
---|
[1078] | 47 |
|
---|
| 48 | print('Available objects:', dir(frams))
|
---|
| 49 | print()
|
---|
| 50 |
|
---|
| 51 | print('Performing a basic test 1/2... ', end='')
|
---|
| 52 | simplest = self.getSimplest("1")
|
---|
| 53 | assert simplest == "X" and type(simplest) is str
|
---|
| 54 | print('OK.')
|
---|
| 55 | print('Performing a basic test 2/2... ', end='')
|
---|
| 56 | assert self.isValid(["X[0:0],", "X[0:0]", "X[1:0]"]) == [False, True, False]
|
---|
| 57 | print('OK.')
|
---|
| 58 | if not self.DETERMINISTIC:
|
---|
[1177] | 59 | frams.Math.randomize()
|
---|
[1101] | 60 | frams.Simulator.expdef = "standard-eval" # this expdef (or fully compatible) must be used by EVALUATION_SETTINGS_FILE
|
---|
[1149] | 61 | if sim_settings_files is not None:
|
---|
| 62 | self.EVALUATION_SETTINGS_FILE = sim_settings_files
|
---|
| 63 | print('Using settings:', self.EVALUATION_SETTINGS_FILE)
|
---|
| 64 | assert isinstance(self.EVALUATION_SETTINGS_FILE, list) # ensure settings file(s) are provided as a list
|
---|
[1196] | 65 |
|
---|
[1149] | 66 | for simfile in self.EVALUATION_SETTINGS_FILE:
|
---|
[1196] | 67 | ec = frams.MessageCatcher.new() # catch potential errors, warnings, messages - just to detect if there are ERRORs
|
---|
| 68 | ec.store = 2; # store all, because they are caught by MessageCatcher and will not appear on console (which we want)
|
---|
[1149] | 69 | frams.Simulator.ximport(simfile, 4 + 8 + 16)
|
---|
[1196] | 70 | ec.close()
|
---|
| 71 | print(ec.messages) # output all caught messages
|
---|
| 72 | assert ec.error_count._value() == 0, "Problem while importing file '%s'" % simfile # make missing files fatal because error messages are easy to overlook
|
---|
[1078] | 73 |
|
---|
| 74 |
|
---|
| 75 | def getSimplest(self, genetic_format) -> str:
|
---|
| 76 | return frams.GenMan.getSimplest(genetic_format).genotype._string()
|
---|
| 77 |
|
---|
| 78 |
|
---|
| 79 | def evaluate(self, genotype_list: List[str]):
|
---|
| 80 | """
|
---|
| 81 | Returns:
|
---|
| 82 | List of dictionaries containing the performance of genotypes evaluated using self.EVALUATION_SETTINGS_FILE.
|
---|
| 83 | Note that for whatever reason (e.g. incorrect genotype), the dictionaries you will get may be empty or
|
---|
| 84 | partially empty and may not have the fields you expected, so handle such cases properly.
|
---|
| 85 | """
|
---|
[1177] | 86 | 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
|
---|
[1078] | 87 |
|
---|
[1084] | 88 | if not self.PRINT_FRAMSTICKS_OUTPUT:
|
---|
| 89 | ec = frams.MessageCatcher.new() # mute potential errors, warnings, messages
|
---|
| 90 |
|
---|
[1078] | 91 | frams.GenePools[0].clear()
|
---|
| 92 | for g in genotype_list:
|
---|
| 93 | frams.GenePools[0].add(g)
|
---|
| 94 | frams.ExpProperties.evalsavefile = "" # no need to store results in a file - we will get evaluations directly from Genotype's "data" field
|
---|
| 95 | frams.Simulator.init()
|
---|
| 96 | frams.Simulator.start()
|
---|
| 97 |
|
---|
[1116] | 98 | # step = frams.Simulator.step # cache reference to avoid repeated lookup in the loop (just for performance)
|
---|
| 99 | # while frams.Simulator.running._int(): # standard-eval.expdef sets running to 0 when the evaluation is complete
|
---|
| 100 | # step()
|
---|
| 101 | frams.Simulator.eval("while(Simulator.running) Simulator.step();") # fastest
|
---|
| 102 | # Timing for evaluating a single simple creature 100x:
|
---|
| 103 | # - python step without caching: 2.2s
|
---|
| 104 | # - python step with caching : 1.6s
|
---|
| 105 | # - pure FramScript and eval() : 0.4s
|
---|
| 106 |
|
---|
[1084] | 107 | if not self.PRINT_FRAMSTICKS_OUTPUT:
|
---|
| 108 | if ec.error_count._value() > 0: # errors are important and should not be ignored, at least display how many
|
---|
[1196] | 109 | print("[ERROR]", ec.error_count, "error(s) and", ec.warning_count-ec.error_count, "warning(s) while evaluating", len(genotype_list), "genotype(s)")
|
---|
[1084] | 110 | ec.close()
|
---|
| 111 |
|
---|
[1078] | 112 | results = []
|
---|
| 113 | for g in frams.GenePools[0]:
|
---|
| 114 | serialized_dict = frams.String.serialize(g.data[frams.ExpProperties.evalsavedata._value()])
|
---|
[1177] | 115 | evaluations = json.loads(serialized_dict._string()) # Framsticks native ExtValue's get converted to native python types such as int, float, list, str.
|
---|
[1078] | 116 | # 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.
|
---|
| 117 | result = {"num": g.num._value(), "name": g.name._value(), "evaluations": evaluations}
|
---|
| 118 | results.append(result)
|
---|
| 119 |
|
---|
| 120 | return results
|
---|
| 121 |
|
---|
| 122 |
|
---|
| 123 | def mutate(self, genotype_list: List[str]) -> List[str]:
|
---|
| 124 | """
|
---|
| 125 | Returns:
|
---|
| 126 | 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).
|
---|
| 127 | """
|
---|
[1177] | 128 | 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
|
---|
[1078] | 129 |
|
---|
| 130 | mutated = []
|
---|
| 131 | for g in genotype_list:
|
---|
| 132 | mutated.append(frams.GenMan.mutate(frams.Geno.newFromString(g)).genotype._string())
|
---|
| 133 | assert len(genotype_list) == len(mutated), "Submitted %d genotypes, received %d validity values" % (len(genotype_list), len(mutated))
|
---|
| 134 | return mutated
|
---|
| 135 |
|
---|
| 136 |
|
---|
| 137 | def crossOver(self, genotype_parent1: str, genotype_parent2: str) -> str:
|
---|
| 138 | """
|
---|
| 139 | Returns:
|
---|
| 140 | The genotype of the offspring. self.GENOTYPE_INVALID if the crossing over failed.
|
---|
| 141 | """
|
---|
| 142 | return frams.GenMan.crossOver(frams.Geno.newFromString(genotype_parent1), frams.Geno.newFromString(genotype_parent2)).genotype._string()
|
---|
| 143 |
|
---|
| 144 |
|
---|
[1177] | 145 | def dissimilarity(self, genotype_list: List[str], method: int) -> np.ndarray:
|
---|
[1078] | 146 | """
|
---|
[1177] | 147 | :param method: -1 = genetic Levenshtein distance; 0, 1, 2 = phenetic dissimilarity (SimilMeasureGreedy, SimilMeasureHungarian, SimilMeasureDistribution)
|
---|
| 148 | :return: A square array with dissimilarities of each pair of genotypes.
|
---|
[1078] | 149 | """
|
---|
[1177] | 150 | 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
|
---|
[1078] | 151 |
|
---|
[1091] | 152 | # if you want to override what EVALUATION_SETTINGS_FILE sets, you can do it below:
|
---|
| 153 | # frams.SimilMeasureHungarian.simil_partgeom = 1
|
---|
| 154 | # frams.SimilMeasureHungarian.simil_weightedMDS = 1
|
---|
[1078] | 155 |
|
---|
| 156 | n = len(genotype_list)
|
---|
| 157 | square_matrix = np.zeros((n, n))
|
---|
| 158 |
|
---|
[1177] | 159 | if method in (0, 1, 2): # Framsticks phenetic dissimilarity methods
|
---|
| 160 | frams.SimilMeasure.simil_type = method
|
---|
| 161 | genos = [] # prepare an array of Geno objects so that we don't need to convert raw strings to Geno objects all the time in loops
|
---|
| 162 | for g in genotype_list:
|
---|
| 163 | genos.append(frams.Geno.newFromString(g))
|
---|
| 164 | frams_evaluateDistance = frams.SimilMeasure.evaluateDistance # cache function reference for better performance in loops
|
---|
| 165 | for i in range(n):
|
---|
| 166 | for j in range(n): # maybe calculate only one triangle if you really need a 2x speedup
|
---|
| 167 | square_matrix[i][j] = frams_evaluateDistance(genos[i], genos[j])._double()
|
---|
| 168 | elif method == -1:
|
---|
| 169 | import Levenshtein
|
---|
| 170 | for i in range(n):
|
---|
| 171 | for j in range(n): # maybe calculate only one triangle if you really need a 2x speedup
|
---|
| 172 | square_matrix[i][j] = Levenshtein.distance(genotype_list[i], genotype_list[j])
|
---|
| 173 | else:
|
---|
| 174 | raise Exception("Don't know what to do with dissimilarity method = %d" % method)
|
---|
| 175 |
|
---|
[1078] | 176 | for i in range(n):
|
---|
| 177 | assert square_matrix[i][i] == 0, "Not a correct dissimilarity matrix, diagonal expected to be 0"
|
---|
[1102] | 178 | non_symmetric_diff = square_matrix - square_matrix.T
|
---|
| 179 | non_symmetric_count = np.count_nonzero(non_symmetric_diff)
|
---|
| 180 | if non_symmetric_count > 0:
|
---|
| 181 | non_symmetric_diff_abs = np.abs(non_symmetric_diff)
|
---|
[1177] | 182 | max_pos1d = np.argmax(non_symmetric_diff_abs) # location of the largest discrepancy
|
---|
| 183 | max_pos2d_XY = np.unravel_index(max_pos1d, non_symmetric_diff_abs.shape) # 2D coordinates of the largest discrepancy
|
---|
| 184 | max_pos2d_YX = max_pos2d_XY[1], max_pos2d_XY[0] # 2D coordinates of the largest discrepancy mirror
|
---|
[1102] | 185 | worst_guy_XY = square_matrix[max_pos2d_XY] # this distance and the other below (its mirror) are most different
|
---|
| 186 | worst_guy_YX = square_matrix[max_pos2d_YX]
|
---|
| 187 | print("[WARN] Dissimilarity matrix: expecting symmetry, but %g out of %d pairs were asymmetrical, max difference was %g (%g %%)" %
|
---|
| 188 | (non_symmetric_count / 2,
|
---|
| 189 | n * (n - 1) / 2,
|
---|
| 190 | non_symmetric_diff_abs[max_pos2d_XY],
|
---|
| 191 | non_symmetric_diff_abs[max_pos2d_XY] * 100 / ((worst_guy_XY + worst_guy_YX) / 2))) # max diff is not necessarily max %
|
---|
[1078] | 192 | return square_matrix
|
---|
| 193 |
|
---|
| 194 |
|
---|
| 195 | def isValid(self, genotype_list: List[str]) -> List[bool]:
|
---|
[1177] | 196 | 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
|
---|
[1078] | 197 | valid = []
|
---|
| 198 | for g in genotype_list:
|
---|
| 199 | valid.append(frams.Geno.newFromString(g).is_valid._int() == 1)
|
---|
[1102] | 200 | assert len(genotype_list) == len(valid), "Tested %d genotypes, received %d validity values" % (len(genotype_list), len(valid))
|
---|
[1078] | 201 | return valid
|
---|
| 202 |
|
---|
| 203 |
|
---|
| 204 | def parseArguments():
|
---|
| 205 | parser = argparse.ArgumentParser(description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[0])
|
---|
[1196] | 206 | parser.add_argument('-path', type=ensureDir, required=True, help='Path to the Framsticks library (.dll or .so or .dylib) without trailing slash.')
|
---|
| 207 | parser.add_argument('-lib', required=False, help='Library name. If not given, "frams-objects.dll" (or .so or .dylib) is assumed depending on the platform.')
|
---|
[1091] | 208 | parser.add_argument('-simsettings', required=False, help='The name of the .sim file with settings for evaluation, mutation, crossover, and similarity estimation. If not given, "eval-allcriteria.sim" is assumed by default. Must be compatible with the "standard-eval" expdef.')
|
---|
[1078] | 209 | 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.')
|
---|
| 210 | return parser.parse_args()
|
---|
| 211 |
|
---|
| 212 |
|
---|
| 213 | def ensureDir(string):
|
---|
| 214 | if os.path.isdir(string):
|
---|
| 215 | return string
|
---|
| 216 | else:
|
---|
| 217 | raise NotADirectoryError(string)
|
---|
| 218 |
|
---|
| 219 |
|
---|
| 220 | if __name__ == "__main__":
|
---|
| 221 | # A demo run.
|
---|
| 222 |
|
---|
| 223 | # TODO ideas:
|
---|
| 224 | # - check_validity with three levels (invalid, corrected, valid)
|
---|
| 225 | # - a pool of binaries running simultaneously, balance load - in particular evaluation
|
---|
| 226 |
|
---|
| 227 | parsed_args = parseArguments()
|
---|
[1102] | 228 | framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.simsettings)
|
---|
[1078] | 229 |
|
---|
[1101] | 230 | print("Sending a direct command to Framsticks library that calculates \"4\"+2 yields", frams.Simulator.eval("return \"4\"+2;"))
|
---|
[1078] | 231 |
|
---|
[1102] | 232 | simplest = framsLib.getSimplest('1' if parsed_args.genformat is None else parsed_args.genformat)
|
---|
[1078] | 233 | print("\tSimplest genotype:", simplest)
|
---|
[1102] | 234 | parent1 = framsLib.mutate([simplest])[0]
|
---|
[1078] | 235 | parent2 = parent1
|
---|
| 236 | MUTATE_COUNT = 10
|
---|
| 237 | for x in range(MUTATE_COUNT): # example of a chain of 10 mutations
|
---|
[1102] | 238 | parent2 = framsLib.mutate([parent2])[0]
|
---|
[1078] | 239 | print("\tParent1 (mutated simplest):", parent1)
|
---|
| 240 | print("\tParent2 (Parent1 mutated %d times):" % MUTATE_COUNT, parent2)
|
---|
[1102] | 241 | offspring = framsLib.crossOver(parent1, parent2)
|
---|
[1078] | 242 | print("\tCrossover (Offspring):", offspring)
|
---|
[1177] | 243 | print('\tDissimilarity of Parent1 and Offspring:', framsLib.dissimilarity([parent1, offspring], 1)[0, 1])
|
---|
[1102] | 244 | print('\tPerformance of Offspring:', framsLib.evaluate([offspring]))
|
---|
| 245 | print('\tValidity of Parent1, Parent 2, and Offspring:', framsLib.isValid([parent1, parent2, offspring]))
|
---|