Changeset 1218 for framspy


Ignore:
Timestamp:
04/17/23 00:31:40 (21 months ago)
Author:
Maciej Komosinski
Message:

Added a function that checks if a genotype produces a valid Creature (with no warnings) and a function that generates random genotypes respecting constraints

File:
1 edited

Legend:

Unmodified
Added
Removed
  • framspy/FramsticksLib.py

    r1204 r1218  
    5959                frams.Simulator.expdef = "standard-eval"  # this expdef (or fully compatible) must be used by EVALUATION_SETTINGS_FILE
    6060                if sim_settings_files is not None:
    61                         self.EVALUATION_SETTINGS_FILE = sim_settings_files.split(";")  # overwrite defaults
     61                        self.EVALUATION_SETTINGS_FILE = sim_settings_files.split(";")  # override defaults. str becomes list
    6262                print('Basic tests OK. Using settings:', self.EVALUATION_SETTINGS_FILE)
    6363                print()
    64                 if not isinstance(self.EVALUATION_SETTINGS_FILE, list):  # ensure settings file(s) are provided as a list
    65                         raise ValueError("Evaluation settings file(s) '%s' should be provided as a list" % self.EVALUATION_SETTINGS_FILE)
    6664
    6765                for simfile in self.EVALUATION_SETTINGS_FILE:
     
    112110                        if ec.error_count._value() > 0:
    113111                                print(ec.messages)  # if errors occurred, output all caught messages for debugging
    114                                 raise RuntimeError("[ERROR] %d error(s) and %d warning(s) while evaluating %d genotype(s)" % (ec.error_count._value(), ec.warning_count._value() - ec.error_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.
     112                                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.
    115113
    116114                results = []
     
    198196
    199197
     198        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):
     199                """
     200                Some algorithms require a "random solution". To this end, this method generates a random framstick genotype.
     201
     202                :param initial_genotype: if not a specific genotype (which could facilitate greater variability of returned genotypes), try `getSimplest(format)`.
     203                :param iter_max: how many mutations can be used to generate a random genotype that fullfills target numbers of parts and neurons.
     204                :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.
     205                :returns: a valid genotype or None if failed and `return_even_if_failed` is False.
     206                """
     207
     208
     209                def estimate_diff(g: str):
     210                        if not self.isValidCreature([g])[0]:
     211                                return None, None
     212                        m = frams.Model.newFromString(g)
     213                        numparts = m.numparts._value()
     214                        numneurons = m.numneurons._value()
     215                        diff_parts = abs(target_parts - numparts)
     216                        diff_neurons = abs(target_neurons - numneurons)
     217                        in_target_range = (parts_min <= numparts <= parts_max) and (neurons_min <= numneurons <= neurons_max)  # less demanding than precisely reaching target_parts and target_neurons
     218                        return diff_parts + diff_neurons, in_target_range
     219
     220
     221                # try to find a genotype that matches the number of parts and neurons randomly selected from the provided min..max range
     222                # (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)
     223                target_parts = np.random.default_rng().integers(parts_min, parts_max + 1)
     224                target_neurons = np.random.default_rng().integers(neurons_min, neurons_max + 1)
     225
     226                if not self.isValidCreature([initial_genotype])[0]:
     227                        raise ValueError("Initial genotype '%s' is invalid" % initial_genotype)
     228
     229                g = initial_genotype
     230                for i in range(iter_max // 2):  # a sequence of iter_max/2 undirected mutations starting from initial_genotype
     231                        g_new = self.mutate([g])[0]
     232                        if self.isValidCreature([g_new])[0]:  # valid mutation
     233                                g = g_new
     234
     235                best_diff, best_in_target_range = estimate_diff(g)
     236                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
     237                        g_new = self.mutate([g])[0]
     238                        diff, in_target_range = estimate_diff(g_new)
     239                        if diff is not None and diff <= best_diff:  # valid mutation and better or as good as current
     240                                g = g_new
     241                                best_diff = diff
     242                                best_in_target_range = in_target_range
     243                # print(diff, best_diff) # print progress approaching target numbers of parts and neurons
     244
     245                if best_in_target_range or return_even_if_failed:
     246                        return g  # best found so far (closest to target numbers of parts and neurons)
     247                return None
     248
     249
    200250        def isValid(self, genotype_list: List[str]) -> List[bool]:
    201251                """
     
    206256                for g in genotype_list:
    207257                        valid.append(frams.Geno.newFromString(g).is_valid._int() == 1)
     258                if len(genotype_list) != len(valid):
     259                        raise RuntimeError("Tested %d genotypes, received %d validity values" % (len(genotype_list), len(valid)))
     260                return valid
     261
     262
     263        def isValidCreature(self, genotype_list: List[str]) -> List[bool]:
     264                """
     265                :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[@][@]'.
     266                """
     267
     268                # 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).
     269                # Thus, the subset of genotypes valid genetically and valid in simulation may be overlapping.
     270                # 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.
     271                # On the other hand, 'X[@][@]' or 'X[|][|]' are valid genetically, but not possible to simulate.
     272                # 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.
     273                # The genetic check in the first lines of the "for" loop makes this function at least as demanding as isValid().
     274
     275                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
     276
     277                pop = frams.Populations[0]  # assuming rules from population #0 (self-colision settings are population-dependent and can influence creature build success/failure)
     278
     279                valid = []
     280                for g in genotype_list:
     281                        if frams.Geno.newFromString(g).is_valid._int() != 1:
     282                                valid.append(False)  # invalid according to genetic check
     283                        else:
     284                                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)
     285                                valid.append(can_add._int() == 1)
     286
    208287                if len(genotype_list) != len(valid):
    209288                        raise RuntimeError("Tested %d genotypes, received %d validity values" % (len(genotype_list), len(valid)))
     
    253332        print('\tPerformance of Offspring:', framsLib.evaluate([offspring]))
    254333        print('\tValidity (genetic) of Parent1, Parent 2, and Offspring:', framsLib.isValid([parent1, parent2, offspring]))
     334        print('\tValidity (simulation) of Parent1, Parent 2, and Offspring:', framsLib.isValidCreature([parent1, parent2, offspring]))
     335        print('\tRandom genotype:', framsLib.getRandomGenotype(simplest, 2, 6, 2, 4, 100, True))
Note: See TracChangeset for help on using the changeset viewer.