source: framspy/FramsticksLib.py @ 1310

Last change on this file since 1310 was 1310, checked in by Maciej Komosinski, 7 days ago

Introduced GENOTYPE_INVALID_OFFSPRING_SUBSTITUTE_ORIGINAL (True by default) for developers who do not specifically detect and handle returned invalid genotypes after mutate() and crossOver()

File size: 24.9 KB
Line 
1from typing import List  # to be able to specify a type hint of list(something)
2from enum import Enum, auto, unique
3import json
4import sys, os
5import argparse
6import random
7import numpy as np
8import frams
9
10
11@unique
12class DissimMethod(Enum):  # values assigned to fields are irrelevant, hence auto()
13        GENE_LEVENSHTEIN = auto()  # genetic Levenshtein distance
14        PHENE_STRUCT_GREEDY = auto()  # phenetic, graph structure, fast but approximate
15        PHENE_STRUCT_OPTIM = auto()  # phenetic, graph structure, slower for complex creatures but optimal
16        PHENE_DESCRIPTORS = auto()  # phenetic, shape descriptors
17        PHENE_DENSITY_COUNT = auto()  # phenetic, density distribution, count of samples
18        PHENE_DENSITY_FREQ = auto()  # phenetic, density distribution, frequency of count of samples
19        FITNESS = auto()  # fitness value
20
21
22class FramsticksLib:
23        """Communicates directly with Framsticks library (.dll or .so or .dylib).
24        You can perform basic operations like mutation, crossover, and evaluation of genotypes.
25        This way you can perform evolution controlled by python as well as access and manipulate genotypes.
26        You can even design and use in evolution your own genetic representation implemented entirely in python,
27        or access and control the simulation and simulated creatures step by step.
28
29        Should you want to modify or extend this class, first see and test the examples in frams-test.py.
30
31        You need to provide one or two parameters when you run this class: the path to Framsticks where .dll/.so/.dylib resides
32        and, optionally, the name of the Framsticks dll/so/dylib (if it is non-standard). See::
33                FramsticksLib.py -h"""
34
35        PRINT_FRAMSTICKS_OUTPUT: bool = False  # set to True for debugging
36        DETERMINISTIC: bool = False  # set to True to have the same results in each run
37
38        GENOTYPE_INVALID = "/*invalid*/"  # this is how genotype invalidity is represented in Framsticks (Geno.format is 'invalid'). Mutation and crossover operators return such a genotype if they were unable to perform their operation (information about the cause is stored in the Geno.info field - see GenMan.cpp)
39        GENOTYPE_INVALID_OFFSPRING_SUBSTITUTE_ORIGINAL = True  # if True, when mutation or crossover is unable to perform their operation for the provided genotype(s), return the original genotype (and print a warning). If this happens extremely rarely, it may be ignored - but if not, you need to identify the reason (e.g., particular genotypes that cause the problem), fix it or change the logic of your algorithm. A more strict approach is to keep this field False - then you must always check if GENOTYPE_INVALID was returned by mutate() or crossOver(), and handle this situation properly (e.g., choose different parent(s) for mutate() or crossOver() and repeat until you get a valid offspring).
40
41        EVALUATION_SETTINGS_FILE = [  # all files MUST be compatible with the standard-eval expdef. The order they are loaded in is important!
42                "eval-allcriteria.sim",  # a good trade-off in performance sampling period ("perfperiod") for vertpos and velocity
43                # "deterministic.sim",  # turns off random noise (added for robustness) so that each evaluation yields identical performance values (causes "overfitting")
44                # "sample-period-2.sim", # short performance sampling period so performance (e.g. vertical position) is sampled more often
45                # "sample-period-longest.sim",  # increased performance sampling period so distance and velocity are measured rectilinearly
46        ]
47
48
49        # This function is not needed because in Python, "For efficiency reasons, each module is only imported once per interpreter session."
50        # @staticmethod
51        # def getFramsModuleInstance():
52        #       """If some other party needs access to the frams module to directly access or modify Framsticks objects,
53        #       use this function to avoid importing the "frams" module multiple times and avoid potentially initializing
54        #       it many times."""
55        #       return frams
56
57        def __init__(self, frams_path, frams_lib_name, sim_settings_files):
58                self.dissim_measure_density_distribution = None  # will be initialized only when necessary (for rare dissimilarity methods)
59
60                if frams_lib_name is None:
61                        frams.init(frams_path)  # could add support for setting alternative directories using -D and -d
62                else:
63                        frams.init(frams_path, "-L" + frams_lib_name)  # could add support for setting alternative directories using -D and -d
64
65                print('Available objects:', dir(frams))
66                print()
67
68                simplest = self.getSimplest("1")
69                if not (simplest == "X" and type(simplest) is str):
70                        raise RuntimeError('Failed getSimplest() test.')
71                if not (self.isValid(["X[0:0],", "X[0:0]", "X[1:0]"]) == [False, True, False]):
72                        raise RuntimeError('Failed isValid() test.')
73
74                if not self.DETERMINISTIC:
75                        frams.Math.randomize()
76                frams.Simulator.expdef = "standard-eval"  # this expdef (or fully compatible) must be used by EVALUATION_SETTINGS_FILE
77                if sim_settings_files is not None:
78                        self.EVALUATION_SETTINGS_FILE = sim_settings_files.split(";")  # override defaults. str becomes list
79                print('Basic tests OK. Using settings:', self.EVALUATION_SETTINGS_FILE)
80                print()
81
82                for simfile in self.EVALUATION_SETTINGS_FILE:
83                        ec = frams.MessageCatcher.new()  # catch potential errors, warnings, messages - just to detect if there are ERRORs
84                        ec.store = 2  # store all, because they are caught by MessageCatcher and will not appear in output (which we want)
85                        frams.Simulator.ximport(simfile, 4 + 8 + 16)
86                        ec.close()
87                        print(ec.messages)  # output all caught messages
88                        if ec.error_count._value() > 0:
89                                raise ValueError("Problem while importing file '%s'" % simfile)  # make missing files or incorrect paths fatal because error messages are easy to overlook in output, and these errors would not prevent Framsticks simulator from performing genetic operations, starting and running in evaluate()
90
91
92        @staticmethod
93        def shortGenotype(genotype: str) -> str:
94                """
95                Returns a few initial characters of the genotype, just for information/identifying the genotype.
96                """
97                return repr(genotype) if len(genotype) <= 10 else repr(genotype[:10] + "...")
98
99
100        def getSimplest(self, genetic_format: str) -> str:
101                return frams.GenMan.getSimplest(genetic_format).genotype._string()
102
103
104        def getPJNC(self, genotype: str):
105                """
106                Returns the number of elements of a phenotype built from the provided genotype (without any simulation).
107
108                :param genotype: the genotype to assess
109                :return: a tuple of (numparts,numjoints,numneurons,numconnections) or None if the genotype is invalid.
110                """
111                model = frams.Model.newFromString(genotype)
112                if model.is_valid._int() == 0:
113                        return None
114                return (model.numparts._int(), model.numjoints._int(), model.numneurons._int(), model.numconnections._int())
115
116
117        def satisfiesConstraints(self, genotype: str, max_numparts: int, max_numjoints: int, max_numneurons: int, max_numconnections: int, max_numgenochars: int) -> bool:
118                """
119                Verifies if the genotype satisfies complexity constraints without actually simulating it.
120                For example, if the genotype represents a phenotype with 1000 Parts, it will be much faster to check it using this function than to simulate the resulting creature using evaluate() only to learn that the number of its Parts exceeds your defined limit.
121
122                :param genotype: the genotype to check
123                :return: False if any constraint is violated or the genotype is invalid, else True. The constraint value of None means no constraint.
124                """
125
126
127                def value_within_constraint(actual_value, constraint_value):
128                        if constraint_value is not None:
129                                if actual_value > constraint_value:
130                                        return False
131                        return True
132
133
134                PJNC = self.getPJNC(genotype)
135                if PJNC is None:
136                        return False  # Let's treat invalid genotypes as not satisfying constraints
137                P, J, N, C = PJNC
138
139                valid = True
140                valid &= value_within_constraint(len(genotype), max_numgenochars)
141                valid &= value_within_constraint(P, max_numparts)
142                valid &= value_within_constraint(J, max_numjoints)
143                valid &= value_within_constraint(N, max_numneurons)
144                valid &= value_within_constraint(C, max_numconnections)
145                return valid
146
147
148        def evaluate(self, genotype_list: List[str]):
149                """
150                Returns:
151                        List of dictionaries containing the performance of genotypes evaluated using self.EVALUATION_SETTINGS_FILE.
152                        Note that for whatever reason (e.g. incorrect genotype), the dictionaries you will get may be empty or
153                        partially empty and may not have the fields you expected, so handle such cases properly.
154                """
155                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
156
157                if not self.PRINT_FRAMSTICKS_OUTPUT:
158                        ec = frams.MessageCatcher.new()  # mute potential errors, warnings, messages
159                        ec.store = 2  # store all, because they are caught by MessageCatcher and will not appear in output
160
161                frams.GenePools[0].clear()
162                for g in genotype_list:
163                        frams.GenePools[0].add(g)
164                frams.ExpProperties.evalsavefile = ""  # no need to store results in a file - we will get evaluations directly from Genotype's "data" field
165                frams.Simulator.init()
166                frams.Simulator.start()
167
168                # step = frams.Simulator.step  # cache reference to avoid repeated lookup in the loop (just for performance)
169                # while frams.Simulator.running._int():  # standard-eval.expdef sets running to 0 when the evaluation is complete
170                #       step()
171                frams.Simulator.eval("while(Simulator.running) Simulator.step();")  # fastest
172                # Timing for evaluating a single simple creature 100x:
173                # - python step without caching: 2.2s
174                # - python step with caching   : 1.6s
175                # - pure FramScript and eval() : 0.4s
176
177                if not self.PRINT_FRAMSTICKS_OUTPUT:
178                        ec.close()
179                        if ec.error_count._value() > 0:
180                                print('\nErrors while evaluating this genotype list:\n', genotype_list, sep='\t')
181                                print(ec.messages)  # if errors occurred, output all caught messages for debugging
182                                raise RuntimeError("[ERROR] %d error(s) and %d warning(s) while evaluating %d genotype(s)" % (ec.error_count._value(), ec.warning_count._value(), len(genotype_list)))  # make errors fatal; by default they stop the simulation anyway so let's not use potentially incorrect or partial results and fix the cause first.
183
184                results = []
185                for g in frams.GenePools[0]:
186                        serialized_dict = frams.String.serialize(g.data[frams.ExpProperties.evalsavedata._value()])
187                        evaluations = json.loads(serialized_dict._string())  # Framsticks native ExtValue's get converted to native python types such as int, float, list, str.
188                        # 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.
189                        result = {"num": g.num._value(), "name": g.name._value(), "evaluations": evaluations}
190                        results.append(result)
191
192                return results
193
194
195        def mutate(self, genotype_list: List[str]) -> List[str]:
196                """
197                Returns:
198                        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).
199                """
200                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
201
202                mutated = []
203                for genotype_parent in genotype_list:
204                        offspring = frams.GenMan.mutate(frams.Geno.newFromString(genotype_parent))
205                        offspring_genotype = offspring.genotype._string()
206                        if offspring_genotype == self.GENOTYPE_INVALID and self.GENOTYPE_INVALID_OFFSPRING_SUBSTITUTE_ORIGINAL:
207                                print('[WARN] mutate(%s) failed but you requested GENOTYPE_INVALID_OFFSPRING_SUBSTITUTE_ORIGINAL, so returning the original genotype instead. Reason for failure: %s' % (self.shortGenotype(genotype_parent), offspring.info._string()))
208                                offspring_genotype = genotype_parent
209                        mutated.append(offspring_genotype)
210                if len(genotype_list) != len(mutated):
211                        raise RuntimeError("Submitted %d genotypes, received %d mutants" % (len(genotype_list), len(mutated)))
212                return mutated
213
214
215        def crossOver(self, genotype_parent1: str, genotype_parent2: str) -> str:
216                """
217                Returns:
218                        The genotype of the offspring. self.GENOTYPE_INVALID if the crossing over failed.
219                """
220                offspring = frams.GenMan.crossOver(frams.Geno.newFromString(genotype_parent1), frams.Geno.newFromString(genotype_parent2))
221                offspring_genotype = offspring.genotype._string()
222                if offspring_genotype == self.GENOTYPE_INVALID and self.GENOTYPE_INVALID_OFFSPRING_SUBSTITUTE_ORIGINAL:
223                        print('[WARN] crossOver(%s, %s) failed but you requested GENOTYPE_INVALID_OFFSPRING_SUBSTITUTE_ORIGINAL, so returning a random parent instead. Reason for failure: %s' % (self.shortGenotype(genotype_parent1), self.shortGenotype(genotype_parent2), offspring.info._string()))
224                        offspring_genotype = random.choice([genotype_parent1, genotype_parent2])
225                return offspring_genotype
226
227
228        def dissimilarity(self, genotype_list: List[str], method: DissimMethod) -> np.ndarray:
229                """
230                        :param method, see DissimMethod.
231                        :return: A square array with dissimilarities of each pair of genotypes.
232                """
233                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
234
235                # if you want to override what EVALUATION_SETTINGS_FILE sets, you can do it below:
236                # frams.SimilMeasureHungarian.simil_partgeom = 1
237                # frams.SimilMeasureHungarian.simil_weightedMDS = 1
238
239                n = len(genotype_list)
240                square_matrix = np.zeros((n, n))
241
242                if method in (DissimMethod.PHENE_STRUCT_GREEDY, DissimMethod.PHENE_STRUCT_OPTIM, DissimMethod.PHENE_DESCRIPTORS):  # Framsticks phenetic dissimilarity methods
243                        frams.SimilMeasure.simil_type = 0 if method == DissimMethod.PHENE_STRUCT_GREEDY else 1 if method == DissimMethod.PHENE_STRUCT_OPTIM else 2
244                        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
245                        for g in genotype_list:
246                                genos.append(frams.Geno.newFromString(g))
247                        frams_evaluateDistance = frams.SimilMeasure.evaluateDistance  # cache function reference for better performance in loops
248                        for i in range(n):
249                                for j in range(n):  # maybe calculate only one triangle if you really need a 2x speedup
250                                        square_matrix[i][j] = frams_evaluateDistance(genos[i], genos[j])._double()
251                elif method == DissimMethod.GENE_LEVENSHTEIN:
252                        import Levenshtein
253                        for i in range(n):
254                                for j in range(n):  # maybe calculate only one triangle if you really need a 2x speedup
255                                        square_matrix[i][j] = Levenshtein.distance(genotype_list[i], genotype_list[j])
256                elif method in (DissimMethod.PHENE_DENSITY_COUNT, DissimMethod.PHENE_DENSITY_FREQ):
257                        if self.dissim_measure_density_distribution is None:
258                                from dissimilarity.density_distribution import DensityDistribution
259                                self.dissim_measure_density_distribution = DensityDistribution(frams)
260                        self.dissim_measure_density_distribution.frequency = (method == DissimMethod.PHENE_DENSITY_FREQ)
261                        square_matrix = self.dissim_measure_density_distribution.getDissimilarityMatrix(genotype_list)
262                else:
263                        raise ValueError("Don't know what to do with dissimilarity method = %s" % method)
264
265                for i in range(n):
266                        assert square_matrix[i][i] == 0, "Not a correct dissimilarity matrix, diagonal expected to be 0"
267                non_symmetric_diff = square_matrix - square_matrix.T
268                non_symmetric_count = np.count_nonzero(non_symmetric_diff)
269                if non_symmetric_count > 0:
270                        non_symmetric_diff_abs = np.abs(non_symmetric_diff)
271                        max_pos1d = np.argmax(non_symmetric_diff_abs)  # location of the largest discrepancy
272                        max_pos2d_XY = np.unravel_index(max_pos1d, non_symmetric_diff_abs.shape)  # 2D coordinates of the largest discrepancy
273                        max_pos2d_YX = max_pos2d_XY[1], max_pos2d_XY[0]  # 2D coordinates of the largest discrepancy mirror
274                        worst_guy_XY = square_matrix[max_pos2d_XY]  # this distance and the other below (its mirror) are most different
275                        worst_guy_YX = square_matrix[max_pos2d_YX]
276                        print("[WARN] Dissimilarity matrix: expecting symmetry, but %g out of %d pairs were asymmetrical, max difference was %g (%g %%)" %
277                              (non_symmetric_count / 2,
278                               n * (n - 1) / 2,
279                               non_symmetric_diff_abs[max_pos2d_XY],
280                               non_symmetric_diff_abs[max_pos2d_XY] * 100 / ((worst_guy_XY + worst_guy_YX) / 2)))  # max diff is not necessarily max %
281                return square_matrix
282
283
284        def getRandomGenotype(self, initial_genotype: str, parts_min: int, parts_max: int, neurons_min: int, neurons_max: int, iter_max: int, return_even_if_failed: bool):
285                """
286                Some algorithms require a "random solution". To this end, this method generates a random framstick genotype.
287
288                :param initial_genotype: if not a specific genotype (which could facilitate greater variability of returned genotypes), try `getSimplest(format)`.
289                :param iter_max: how many mutations can be used to generate a random genotype that fullfills target numbers of parts and neurons.
290                :param return_even_if_failed: if the target numbers of parts and neurons was not achieved, return the closest genotype that was found? Set it to False first to experimentally adjust `iter_max` so that in most calls this function returns a genotype with target numbers of parts and neurons, and then you can set this parameter to True if target numbers of parts and neurons are not absolutely required.
291                :returns: a valid genotype or None if failed and `return_even_if_failed` is False.
292                """
293
294
295                def estimate_diff(g: str):
296                        if not self.isValidCreature([g])[0]:
297                                return None, None
298                        m = frams.Model.newFromString(g)
299                        numparts = m.numparts._value()
300                        numneurons = m.numneurons._value()
301                        diff_parts = abs(target_parts - numparts)
302                        diff_neurons = abs(target_neurons - numneurons)
303                        in_target_range = (parts_min <= numparts <= parts_max) and (neurons_min <= numneurons <= neurons_max)  # less demanding than precisely reaching target_parts and target_neurons
304                        return diff_parts + diff_neurons, in_target_range
305
306
307                # try to find a genotype that matches the number of parts and neurons randomly selected from the provided min..max range
308                # (even if we fail to match this precise target, the goal will be achieved if the found genotype manages to be within min..max ranges for parts and neurons)
309                target_parts = np.random.default_rng().integers(parts_min, parts_max + 1)
310                target_neurons = np.random.default_rng().integers(neurons_min, neurons_max + 1)
311
312                if not self.isValidCreature([initial_genotype])[0]:
313                        raise ValueError("Initial genotype '%s' is invalid" % initial_genotype)
314
315                g = initial_genotype
316                for i in range(iter_max // 2):  # a sequence of iter_max/2 undirected mutations starting from initial_genotype
317                        g_new = self.mutate([g])[0]
318                        if self.isValidCreature([g_new])[0]:  # valid mutation
319                                g = g_new
320
321                best_diff, best_in_target_range = estimate_diff(g)
322                for i in range(iter_max // 2):  # a sequence of iter_max/2 mutations, only accepting those which approach target numbers of parts and neurons
323                        g_new = self.mutate([g])[0]
324                        diff, in_target_range = estimate_diff(g_new)
325                        if diff is not None and diff <= best_diff:  # valid mutation and better or as good as current
326                                g = g_new
327                                best_diff = diff
328                                best_in_target_range = in_target_range
329                # print(diff, best_diff) # print progress approaching target numbers of parts and neurons
330
331                if best_in_target_range or return_even_if_failed:
332                        return g  # best found so far (closest to target numbers of parts and neurons)
333                return None
334
335
336        def isValid(self, genotype_list: List[str]) -> List[bool]:
337                """
338                :returns: genetic validity (i.e., not based on trying to build creatures from provided genotypes). For a more thorough check, see isValidCreature().
339                """
340                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
341                valid = []
342                for g in genotype_list:
343                        valid.append(frams.Geno.newFromString(g).is_valid._int() == 1)
344                if len(genotype_list) != len(valid):
345                        raise RuntimeError("Tested %d genotypes, received %d validity values" % (len(genotype_list), len(valid)))
346                return valid
347
348
349        def isValidCreature(self, genotype_list: List[str]) -> List[bool]:
350                """
351                :returns: validity of the genotype when revived. Apart from genetic validity, this includes detecting problems that may arise when building a Creature from Genotype, such as multiple muscles of the same type in the same location in body, e.g. 'X[@][@]'.
352                """
353
354                # Genetic validity and simulator validity are two separate properties (in particular, genetic validity check is implemented by the author of a given genetic format and operators).
355                # Thus, the subset of genotypes valid genetically and valid in simulation may be overlapping.
356                # For example, 'X[]' or 'Xr' are considered invalid by the genetic checker, but the f1->f0 converter will ignore meaningless genes and produce a valid f0 genotype.
357                # On the other hand, 'X[@][@]' or 'X[|][|]' are valid genetically, but not possible to simulate.
358                # For simplicity of usage (so that one does not need to check both properties separately using both functions), let's make one validity a subset of the other.
359                # The genetic check in the first lines of the "for" loop makes this function at least as demanding as isValid().
360
361                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
362
363                pop = frams.Populations[0]  # assuming rules from population #0 (self-colision settings are population-dependent and can influence creature build success/failure)
364
365                valid = []
366                for g in genotype_list:
367                        if frams.Geno.newFromString(g).is_valid._int() != 1:
368                                valid.append(False)  # invalid according to genetic check
369                        else:
370                                can_add = pop.canAdd(g, 1, 1)  # First "1" means to treat warnings during build as build failures - this allows detecting problems when building Creature from Genotype. Second "1" means mute emitted errors, warnings, messages. Returns 1 (ok, could add) or 0 (there were some problems building Creature from Genotype)
371                                valid.append(can_add._int() == 1)
372
373                if len(genotype_list) != len(valid):
374                        raise RuntimeError("Tested %d genotypes, received %d validity values" % (len(genotype_list), len(valid)))
375                return valid
376
377
378def parseArguments():
379        parser = argparse.ArgumentParser(description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[0])
380        parser.add_argument('-path', type=ensureDir, required=True, help='Path to the Framsticks library (.dll or .so or .dylib) without trailing slash.')
381        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.')
382        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. If you want to provide more files, separate them with a semicolon ';'.")
383        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.')
384        return parser.parse_args()
385
386
387def ensureDir(string):
388        if os.path.isdir(string):
389                return string
390        else:
391                raise NotADirectoryError(string)
392
393
394if __name__ == "__main__":
395        # A demo run.
396
397        # TODO ideas:
398        # - check_validity with three levels (invalid, corrected, valid)
399        # - a pool of binaries running simultaneously, balance load - in particular evaluation
400
401        parsed_args = parseArguments()
402        framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.simsettings)
403
404        print("Sending a direct command to Framsticks library that calculates \"4\"+2 yields", frams.Simulator.eval("return \"4\"+2;"))
405
406        simplest = framsLib.getSimplest('1' if parsed_args.genformat is None else parsed_args.genformat)
407        print("\tSimplest genotype:", simplest)
408        parent1 = framsLib.mutate([simplest])[0]
409        parent2 = parent1
410        MUTATE_COUNT = 10
411        for x in range(MUTATE_COUNT):  # example of a chain of 10 mutations
412                parent2 = framsLib.mutate([parent2])[0]
413        print("\tParent1 (mutated simplest):", parent1)
414        print("\tParent2 (Parent1 mutated %d times):" % MUTATE_COUNT, parent2)
415        offspring = framsLib.crossOver(parent1, parent2)
416        print("\tCrossover (Offspring):", offspring)
417        print('\tDissimilarity of Parent1 and Offspring:', framsLib.dissimilarity([parent1, offspring], DissimMethod.PHENE_STRUCT_OPTIM)[0, 1])
418        print('\tPerformance of Offspring:', framsLib.evaluate([offspring]))
419        print('\tValidity (genetic) of Parent1, Parent 2, and Offspring:', framsLib.isValid([parent1, parent2, offspring]))
420        print('\tValidity (simulation) of Parent1, Parent 2, and Offspring:', framsLib.isValidCreature([parent1, parent2, offspring]))
421        print('\tValidity (constraints) of Offspring:', framsLib.satisfiesConstraints(offspring, 2, None, 5, 10, None))
422        print('\tRandom genotype:', framsLib.getRandomGenotype(simplest, 2, 6, 2, 4, 100, True))
Note: See TracBrowser for help on using the repository browser.