11/29/21 03:33:54 (2 years ago)
Maciej Komosinski

Multiple new functionalities in simple evolutionary optimization in python; added many new parameters; added some examples

1 edited


  • framspy/FramsticksEvolution.py

    r1117 r1161  
    66from FramsticksLib import FramsticksLib
    89# Note: this may be less efficient than running the evolution directly in Framsticks, so if performance is key, compare both options.
    11 # The list of criteria includes 'vertpos', 'velocity', 'distance', 'vertvel', 'lifespan', 'numjoints', 'numparts', 'numneurons', 'numconnections'.
    12 OPTIMIZATION_CRITERIA = ['velocity']  # Single or multiple criteria. Names from the standard-eval.expdef dictionary, e.g. ['vertpos', 'velocity'].
     12def genotype_within_constraint(criterion_actual_value, constraint_value):
     13        if constraint_value is not None:
     14                if criterion_actual_value > constraint_value:
     15                        return False
     16        return True
    1721        data = frams_cli.evaluate([genotype])
    1822        # print("Evaluated '%s'" % genotype, 'evaluation is:', data)
     23        valid = True
    1924        try:
    2025                first_genotype_data = data[0]
    2328                fitness = [default_evaluation_data[crit] for crit in OPTIMIZATION_CRITERIA]
    2429        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 for some other reason
    25                 fitness = [-1] * len(OPTIMIZATION_CRITERIA)  # fitness of -1 is intended to discourage further propagation of this genotype via selection ("this one is very poor")
     30                valid = False
    2631                print('Error "%s": could not evaluate genotype "%s", returning fitness %s' % (str(e), genotype, fitness))
     32        if valid:
     33                valid &= genotype_within_constraint(default_evaluation_data['numparts'], parsed_args.max_numparts)
     34                valid &= genotype_within_constraint(default_evaluation_data['numjoints'], parsed_args.max_numjoints)
     35                valid &= genotype_within_constraint(default_evaluation_data['numneurons'], parsed_args.max_numneurons)
     36                valid &= genotype_within_constraint(default_evaluation_data['numconnections'], parsed_args.max_numconnections)
     37                valid &= genotype_within_constraint(len(genotype), parsed_args.max_numgenochars)
     38        if not valid:
     39                fitness = [-1] * len(OPTIMIZATION_CRITERIA)  # fitness of -1 is intended to discourage further propagation of this genotype via selection ("this genotype is very poor")
    2740        return fitness
    43 def frams_getsimplest(frams_cli, genetic_format):
    44         return frams_cli.getSimplest(genetic_format)
     56def frams_getsimplest(frams_cli, genetic_format, initial_genotype):
     57        return initial_genotype if initial_genotype is not None else frams_cli.getSimplest(genetic_format)
    47 def prepareToolbox(frams_cli, genetic_format):
     60def prepareToolbox(frams_cli, tournament_size, genetic_format, initial_genotype):
    4861        creator.create("FitnessMax", base.Fitness, weights=[1.0] * len(OPTIMIZATION_CRITERIA))
    4962        creator.create("Individual", list, fitness=creator.FitnessMax)  # would be nice to have "str" instead of unnecessary "list of str"
    5164        toolbox = base.Toolbox()
    52         toolbox.register("attr_simplest_genotype", frams_getsimplest, frams_cli, genetic_format)  # "Attribute generator"
     65        toolbox.register("attr_simplest_genotype", frams_getsimplest, frams_cli, genetic_format, initial_genotype)  # "Attribute generator"
    5366        # (failed) struggle to have an individual which is a simple str, not a list of str
    5467        # toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_frams)
    6376        toolbox.register("mutate", frams_mutate, frams_cli)
    6477        if len(OPTIMIZATION_CRITERIA) <= 1:
    65                 toolbox.register("select", tools.selTournament, tournsize=5)
     78                toolbox.register("select", tools.selTournament, tournsize=tournament_size)
    6679        else:
    6780                toolbox.register("select", tools.selNSGA2)
    7386        parser.add_argument('-path', type=ensureDir, required=True, help='Path to Framsticks CLI without trailing slash.')
    7487        parser.add_argument('-lib', required=False, help='Library name. If not given, "frams-objects.dll" or "frams-objects.so" is assumed depending on the platform.')
    75         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.')
    76         parser.add_argument('-genformat', required=False, help='Genetic format for the demo run, for example 4, 9, or B. If not given, f1 is assumed.')
     88        parser.add_argument('-sim', required=False, default="eval-allcriteria.sim", 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 ';'.")
     90        parser.add_argument('-genformat', required=False, help='Genetic format for the simplest initial genotype, for example 4, 9, or B. If not given, f1 is assumed.')
     91        parser.add_argument('-initialgenotype', required=False, help='The genotype used to seed the initial population. If given, the -genformat argument is ignored.')
     93        parser.add_argument('-opt', required=True, help='optimization criteria: vertpos, velocity, distance, vertvel, lifespan, numjoints, numparts, numneurons, numconnections (or other as long as it is provided by the .sim file and its .expdef). For multiple criteria optimization, separate the names by the comma.')
     94        parser.add_argument('-popsize', type=int, default=50, help="Population size, default: 50.")
     95        parser.add_argument('-generations', type=int, default=5, help="Number of generations, default: 5.")
     96        parser.add_argument('-tournament', type=int, default=5, help="Tournament size, default: 5.")
     97        parser.add_argument('-pmut', type=float, default=0.9, help="Probability of mutation, default: 0.9")
     98        parser.add_argument('-pxov', type=float, default=0.2, help="Probability of crossover, default: 0.2")
     99        parser.add_argument('-hof_size', type=int, default=10, help="Number of genotypes in Hall of Fame. Default: 10.")
     100        parser.add_argument('-hof_savefile', required=False, help='If set, Hall of Fame will be saved in Framsticks file format (recommended extension *.gen).')
     102        parser.add_argument('-max_numparts', type=int, default=None, help="Maximum number of Parts. Default: no limit")
     103        parser.add_argument('-max_numjoints', type=int, default=None, help="Maximum number of Joints. Default: no limit")
     104        parser.add_argument('-max_numneurons', type=int, default=None, help="Maximum number of Neurons. Default: no limit")
     105        parser.add_argument('-max_numconnections', type=int, default=None, help="Maximum number of Neural connections. Default: no limit")
     106        parser.add_argument('-max_numgenochars', type=int, default=None, help="Maximum number of characters in genotype (including the format prefix, if any). Default: no limit")
    77107        return parser.parse_args()
     117def save_genotypes(filename, OPTIMIZATION_CRITERIA, hof):
     118        from framsfiles import writer as framswriter
     119        with open(filename, "w") as outfile:
     120                for ind in hof:
     121                        keyval = {}
     122                        for i, k in enumerate(OPTIMIZATION_CRITERIA):  # construct a dictionary with criteria names and their values
     123                                keyval[k] = ind.fitness.values[i]  # TODO it would be better to save in Individual (after evaluation) all fields returned by Framsticks, and get these fields here, not just the criteria that were actually used as fitness in evolution.
     124                        # Note: prior to the release of Framsticks 5.0, saving e.g. numparts (i.e. P) without J,N,C breaks re-calcucation of P,J,N,C in Framsticks and they appear to be zero (nothing serious).
     125                        outfile.write(framswriter.from_collection({"_classname": "org", "genotype": ind[0], **keyval}))
     126                        outfile.write("\n")
     127        print("Saved '%s' (%d)" % (filename, len(hof)))
    87130if __name__ == "__main__":
    88         # A demo run: optimize OPTIMIZATION_CRITERIA
    90131        # random.seed(123)  # see FramsticksLib.DETERMINISTIC below, set to True if you want full determinism
    91132        FramsticksLib.DETERMINISTIC = False  # must be set before FramsticksLib() constructor call
    92133        parsed_args = parseArguments()
    93         framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.simsettings)
     134        print("Argument values:", ", ".join(['%s=%s' % (arg, getattr(parsed_args, arg)) for arg in vars(parsed_args)]))
    95         toolbox = prepareToolbox(framsLib, '1' if parsed_args.genformat is None else parsed_args.genformat)
     136        OPTIMIZATION_CRITERIA = parsed_args.opt.split(",")
     137        framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.sim.split(";"))
    97         POPSIZE = 20
    98         GENERATIONS = 50
     139        toolbox = prepareToolbox(framsLib, parsed_args.tournament, '1' if parsed_args.genformat is None else parsed_args.genformat, parsed_args.initialgenotype)
    100         pop = toolbox.population(n=POPSIZE)
    101         hof = tools.HallOfFame(5)
     141        pop = toolbox.population(n=parsed_args.popsize)
     142        hof = tools.HallOfFame(parsed_args.hof_size)
    102143        stats = tools.Statistics(lambda ind: ind.fitness.values)
    103144        stats.register("avg", np.mean)
    106147        stats.register("max", np.max)
    108         print('Evolution with population size %d for %d generations, optimization criteria: %s' % (POPSIZE, GENERATIONS, OPTIMIZATION_CRITERIA))
    109         pop, log = algorithms.eaSimple(pop, toolbox, cxpb=0.2, mutpb=0.9, ngen=GENERATIONS, stats=stats, halloffame=hof, verbose=True)
     149        pop, log = algorithms.eaSimple(pop, toolbox, cxpb=parsed_args.pxov, mutpb=parsed_args.pmut, ngen=parsed_args.generations, stats=stats, halloffame=hof, verbose=True)
    110150        print('Best individuals:')
    111         for best in hof:
    112                 print(best.fitness, '\t-->\t', best[0])
     151        for ind in hof:
     152                print(ind.fitness, '\t-->\t', ind[0])
     154        if parsed_args.hof_savefile is not None:
     155                save_genotypes(parsed_args.hof_savefile, OPTIMIZATION_CRITERIA, hof)
Note: See TracChangeset for help on using the changeset viewer.