Changeset 1311


Ignore:
Timestamp:
07/05/24 02:02:20 (3 days ago)
Author:
Maciej Komosinski
Message:

Introduced FITNESS_VALUE_INFEASIBLE_SOLUTION and modified selection and statistics so that infeasible solutions are explicitly excluded (previously they were not excluded from statistics calculation, and eliminating/discriminating them during selection only relied on their relatively poor fitness value)

File:
1 edited

Legend:

Unmodified
Added
Removed
  • framspy/FramsticksEvolution.py

    r1293 r1311  
    66from FramsticksLib import FramsticksLib
    77
     8# Note: this may be less efficient than running the evolution directly in Framsticks, so if performance is key, compare both options.
    89
    9 # Note: this may be less efficient than running the evolution directly in Framsticks, so if performance is key, compare both options.
     10
     11FITNESS_VALUE_INFEASIBLE_SOLUTION = -999999.0  # DEAP expects fitness to always be a real value (not None), so this special value indicates that a solution is invalid, incorrect, or infeasible. [Related: https://github.com/DEAP/deap/issues/30 ]. Using float('-inf') or -sys.float_info.max here causes DEAP to silently exit. If you are not using DEAP, set this constant to None, float('nan'), or another special/non-float value to avoid clashing with valid real fitness values, and handle such solutions appropriately as a separate case.
    1012
    1113
     
    1618                if actual_value > constraint_value:
    1719                        if REPORT_CONSTRAINT_VIOLATIONS:
    18                                 print('Genotype "%s" assigned low fitness because it violates constraint "%s": %s exceeds threshold %s' % (genotype, criterion_name, actual_value, constraint_value))
     20                                print('Genotype "%s" assigned a special ("infeasible solution") fitness because it violates constraint "%s": %s exceeds the threshold of %s' % (genotype, criterion_name, actual_value, constraint_value))
    1921                        return False
    2022        return True
     
    2224
    2325def frams_evaluate(frams_lib, individual):
    24         BAD_FITNESS = [-1] * len(OPTIMIZATION_CRITERIA)  # fitness of -1 is intended to discourage further propagation of this genotype via selection ("this genotype is very poor")
    25         genotype = individual[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
     26        FITNESS_CRITERIA_INFEASIBLE_SOLUTION = [FITNESS_VALUE_INFEASIBLE_SOLUTION] * len(OPTIMIZATION_CRITERIA)  # this special fitness value indicates that the solution should not be propagated via selection ("that genotype is invalid"). The floating point value is only used for compatibility with DEAP. If you implement your own optimization algorithm, instead of a negative value in this constant, use a special value like None to properly distinguish between feasible and infeasible solutions.
     27        genotype = individual[0]  # individual[0] because we can't (?) have a simple str as a DEAP genotype/individual, only list of str.
    2628        data = frams_lib.evaluate([genotype])
    2729        # print("Evaluated '%s'" % genotype, 'evaluation is:', data)
     
    3436        except (KeyError, TypeError) as e:  # the evaluation may have failed for an invalid genotype (such as X[@][@] with "Don't simulate genotypes with warnings" option), or because the creature failed to stabilize, or for some other reason
    3537                valid = False
    36                 print('Problem "%s" so could not evaluate genotype "%s", hence assigned it low fitness: %s' % (str(e), genotype, BAD_FITNESS))
     38                print('Problem "%s" so could not evaluate genotype "%s", hence assigned it a special ("infeasible solution") fitness value: %s' % (str(e), genotype, FITNESS_CRITERIA_INFEASIBLE_SOLUTION))
    3739        if valid:
    3840                default_evaluation_data['numgenocharacters'] = len(genotype)  # for consistent constraint checking below
     
    4345                valid &= genotype_within_constraint(genotype, default_evaluation_data, 'numgenocharacters', parsed_args.max_numgenochars)
    4446        if not valid:
    45                 fitness = BAD_FITNESS
     47                fitness = FITNESS_CRITERIA_INFEASIBLE_SOLUTION
    4648        return fitness
    4749
    4850
    4951def frams_crossover(frams_lib, individual1, individual2):
    50         geno1 = individual1[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
    51         geno2 = individual2[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
     52        geno1 = individual1[0]  # individual[0] because we can't (?) have a simple str as a DEAP genotype/individual, only list of str.
     53        geno2 = individual2[0]  # individual[0] because we can't (?) have a simple str as a DEAP genotype/individual, only list of str.
    5254        individual1[0] = frams_lib.crossOver(geno1, geno2)
    5355        individual2[0] = frams_lib.crossOver(geno1, geno2)
     
    5658
    5759def frams_mutate(frams_lib, individual):
    58         individual[0] = frams_lib.mutate([individual[0]])[0]  # individual[0] because we can't (?) have a simple str as a deap genotype/individual, only list of str.
     60        individual[0] = frams_lib.mutate([individual[0]])[0]  # individual[0] because we can't (?) have a simple str as a DEAP genotype/individual, only list of str.
    5961        return individual,
    6062
     
    6264def frams_getsimplest(frams_lib, genetic_format, initial_genotype):
    6365        return initial_genotype if initial_genotype is not None else frams_lib.getSimplest(genetic_format)
     66
     67
     68def is_feasible_fitness_value(fitness_value: float) -> bool:
     69        assert isinstance(fitness_value, float), f"feasible_fitness({fitness_value}): argument is not of type float, it is of type {type(fitness_value)}"  # since we are using DEAP, we unfortunately must represent the fitness of an "infeasible solution" as a float...
     70        return fitness_value != FITNESS_VALUE_INFEASIBLE_SOLUTION  # ...so if a valid solution happens to have fitness equal to this special value, such a solution will be considered infeasible :/
     71
     72
     73def is_feasible_fitness_criteria(fitness_criteria: tuple) -> bool:
     74        return all(is_feasible_fitness_value(fitness_value) for fitness_value in fitness_criteria)
     75
     76
     77def select_feasible(individuals):
     78        """
     79        Filters out only feasible individuals (i.e., with fitness different from FITNESS_OF_INFEASIBLE_SOLUTION)
     80        """
     81        # for ind in individuals:
     82        #       print(ind.fitness.values, ind)
     83        feasible_individuals = [ind for ind in individuals if is_feasible_fitness_criteria(ind.fitness.values)]
     84        count_all = len(individuals)
     85        count_infeasible = count_all - len(feasible_individuals)
     86        if count_infeasible != 0:
     87                print("Selection: ignoring %d infeasible solution%s in a population of size %d" % (count_infeasible, 's' if count_infeasible > 1 else '', count_all))
     88        return feasible_individuals
     89
     90
     91def selTournament_only_feasible(individuals, k, tournsize):
     92        return tools.selTournament(select_feasible(individuals), k, tournsize=tournsize)
     93
     94
     95def selNSGA2_only_feasible(individuals, k):
     96        return tools.selNSGA2(select_feasible(individuals), k)  # this method (unfortunately) decreases population size permanently each time an infeasible solution is removed
    6497
    6598
     
    82115        toolbox.register("mutate", frams_mutate, frams_lib)
    83116        if len(OPTIMIZATION_CRITERIA) <= 1:
    84                 toolbox.register("select", tools.selTournament, tournsize=tournament_size)
     117                # toolbox.register("select", tools.selTournament, tournsize=tournament_size) # without explicitly filtering out infeasible solutions - eliminating/discriminating infeasible solutions during selection would only rely on their relatively poor fitness value
     118                toolbox.register("select", selTournament_only_feasible, tournsize=tournament_size)
    85119        else:
    86                 toolbox.register("select", tools.selNSGA2)
     120                # toolbox.register("select", selNSGA2) # without explicitly filtering out infeasible solutions - eliminating/discriminating infeasible solutions during selection would only rely on their relatively poor fitness value
     121                toolbox.register("select", selNSGA2_only_feasible)
    87122        return toolbox
    88123
     
    147182        hof = tools.HallOfFame(parsed_args.hof_size)
    148183        stats = tools.Statistics(lambda ind: ind.fitness.values)
    149         stats.register("avg", np.mean)
    150         stats.register("stddev", np.std)
    151         stats.register("min", np.min)
    152         stats.register("max", np.max)
     184        # calculate statistics excluding infeasible solutions (by filtering out their fitness=FITNESS_OF_INFEASIBLE_SOLUTION)
     185        filter_feasible_for_function = lambda function, fitness_criteria: function(list(filter(is_feasible_fitness_criteria, fitness_criteria)))
     186        stats.register("avg", lambda fitness_criteria: filter_feasible_for_function(np.mean, fitness_criteria))
     187        stats.register("stddev", lambda fitness_criteria: filter_feasible_for_function(np.std, fitness_criteria))
     188        stats.register("min", lambda fitness_criteria: filter_feasible_for_function(np.min, fitness_criteria))
     189        stats.register("max", lambda fitness_criteria: filter_feasible_for_function(np.max, fitness_criteria))
    153190        pop, log = algorithms.eaSimple(pop, toolbox, cxpb=parsed_args.pxov, mutpb=parsed_args.pmut, ngen=parsed_args.generations, stats=stats, halloffame=hof, verbose=True)
    154191        print('Best individuals:')
Note: See TracChangeset for help on using the changeset viewer.