Index: /framspy/FramsticksEvolution.py
===================================================================
--- /framspy/FramsticksEvolution.py	(revision 1160)
+++ /framspy/FramsticksEvolution.py	(revision 1161)
@@ -6,9 +6,13 @@
 from FramsticksLib import FramsticksLib
 
+
 # Note: this may be less efficient than running the evolution directly in Framsticks, so if performance is key, compare both options.
 
 
-# The list of criteria includes 'vertpos', 'velocity', 'distance', 'vertvel', 'lifespan', 'numjoints', 'numparts', 'numneurons', 'numconnections'.
-OPTIMIZATION_CRITERIA = ['velocity']  # Single or multiple criteria. Names from the standard-eval.expdef dictionary, e.g. ['vertpos', 'velocity'].
+def genotype_within_constraint(criterion_actual_value, constraint_value):
+	if constraint_value is not None:
+		if criterion_actual_value > constraint_value:
+			return False
+	return True
 
 
@@ -17,4 +21,5 @@
 	data = frams_cli.evaluate([genotype])
 	# print("Evaluated '%s'" % genotype, 'evaluation is:', data)
+	valid = True
 	try:
 		first_genotype_data = data[0]
@@ -23,6 +28,14 @@
 		fitness = [default_evaluation_data[crit] for crit in OPTIMIZATION_CRITERIA]
 	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
-		fitness = [-1] * len(OPTIMIZATION_CRITERIA)  # fitness of -1 is intended to discourage further propagation of this genotype via selection ("this one is very poor")
+		valid = False
 		print('Error "%s": could not evaluate genotype "%s", returning fitness %s' % (str(e), genotype, fitness))
+	if valid:
+		valid &= genotype_within_constraint(default_evaluation_data['numparts'], parsed_args.max_numparts)
+		valid &= genotype_within_constraint(default_evaluation_data['numjoints'], parsed_args.max_numjoints)
+		valid &= genotype_within_constraint(default_evaluation_data['numneurons'], parsed_args.max_numneurons)
+		valid &= genotype_within_constraint(default_evaluation_data['numconnections'], parsed_args.max_numconnections)
+		valid &= genotype_within_constraint(len(genotype), parsed_args.max_numgenochars)
+	if not valid:
+		fitness = [-1] * len(OPTIMIZATION_CRITERIA)  # fitness of -1 is intended to discourage further propagation of this genotype via selection ("this genotype is very poor")
 	return fitness
 
@@ -41,14 +54,14 @@
 
 
-def frams_getsimplest(frams_cli, genetic_format):
-	return frams_cli.getSimplest(genetic_format)
+def frams_getsimplest(frams_cli, genetic_format, initial_genotype):
+	return initial_genotype if initial_genotype is not None else frams_cli.getSimplest(genetic_format)
 
 
-def prepareToolbox(frams_cli, genetic_format):
+def prepareToolbox(frams_cli, tournament_size, genetic_format, initial_genotype):
 	creator.create("FitnessMax", base.Fitness, weights=[1.0] * len(OPTIMIZATION_CRITERIA))
 	creator.create("Individual", list, fitness=creator.FitnessMax)  # would be nice to have "str" instead of unnecessary "list of str"
 
 	toolbox = base.Toolbox()
-	toolbox.register("attr_simplest_genotype", frams_getsimplest, frams_cli, genetic_format)  # "Attribute generator"
+	toolbox.register("attr_simplest_genotype", frams_getsimplest, frams_cli, genetic_format, initial_genotype)  # "Attribute generator"
 	# (failed) struggle to have an individual which is a simple str, not a list of str
 	# toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_frams)
@@ -63,5 +76,5 @@
 	toolbox.register("mutate", frams_mutate, frams_cli)
 	if len(OPTIMIZATION_CRITERIA) <= 1:
-		toolbox.register("select", tools.selTournament, tournsize=5)
+		toolbox.register("select", tools.selTournament, tournsize=tournament_size)
 	else:
 		toolbox.register("select", tools.selNSGA2)
@@ -73,6 +86,23 @@
 	parser.add_argument('-path', type=ensureDir, required=True, help='Path to Framsticks CLI without trailing slash.')
 	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.')
-	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.')
-	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.')
+	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 ';'.")
+
+	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.')
+	parser.add_argument('-initialgenotype', required=False, help='The genotype used to seed the initial population. If given, the -genformat argument is ignored.')
+
+	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.')
+	parser.add_argument('-popsize', type=int, default=50, help="Population size, default: 50.")
+	parser.add_argument('-generations', type=int, default=5, help="Number of generations, default: 5.")
+	parser.add_argument('-tournament', type=int, default=5, help="Tournament size, default: 5.")
+	parser.add_argument('-pmut', type=float, default=0.9, help="Probability of mutation, default: 0.9")
+	parser.add_argument('-pxov', type=float, default=0.2, help="Probability of crossover, default: 0.2")
+	parser.add_argument('-hof_size', type=int, default=10, help="Number of genotypes in Hall of Fame. Default: 10.")
+	parser.add_argument('-hof_savefile', required=False, help='If set, Hall of Fame will be saved in Framsticks file format (recommended extension *.gen).')
+
+	parser.add_argument('-max_numparts', type=int, default=None, help="Maximum number of Parts. Default: no limit")
+	parser.add_argument('-max_numjoints', type=int, default=None, help="Maximum number of Joints. Default: no limit")
+	parser.add_argument('-max_numneurons', type=int, default=None, help="Maximum number of Neurons. Default: no limit")
+	parser.add_argument('-max_numconnections', type=int, default=None, help="Maximum number of Neural connections. Default: no limit")
+	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")
 	return parser.parse_args()
 
@@ -85,19 +115,30 @@
 
 
+def save_genotypes(filename, OPTIMIZATION_CRITERIA, hof):
+	from framsfiles import writer as framswriter
+	with open(filename, "w") as outfile:
+		for ind in hof:
+			keyval = {}
+			for i, k in enumerate(OPTIMIZATION_CRITERIA):  # construct a dictionary with criteria names and their values
+				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.
+			# 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).
+			outfile.write(framswriter.from_collection({"_classname": "org", "genotype": ind[0], **keyval}))
+			outfile.write("\n")
+	print("Saved '%s' (%d)" % (filename, len(hof)))
+
+
 if __name__ == "__main__":
-	# A demo run: optimize OPTIMIZATION_CRITERIA
-
 	# random.seed(123)  # see FramsticksLib.DETERMINISTIC below, set to True if you want full determinism
 	FramsticksLib.DETERMINISTIC = False  # must be set before FramsticksLib() constructor call
 	parsed_args = parseArguments()
-	framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.simsettings)
+	print("Argument values:", ", ".join(['%s=%s' % (arg, getattr(parsed_args, arg)) for arg in vars(parsed_args)]))
 
-	toolbox = prepareToolbox(framsLib, '1' if parsed_args.genformat is None else parsed_args.genformat)
+	OPTIMIZATION_CRITERIA = parsed_args.opt.split(",")
+	framsLib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.sim.split(";"))
 
-	POPSIZE = 20
-	GENERATIONS = 50
+	toolbox = prepareToolbox(framsLib, parsed_args.tournament, '1' if parsed_args.genformat is None else parsed_args.genformat, parsed_args.initialgenotype)
 
-	pop = toolbox.population(n=POPSIZE)
-	hof = tools.HallOfFame(5)
+	pop = toolbox.population(n=parsed_args.popsize)
+	hof = tools.HallOfFame(parsed_args.hof_size)
 	stats = tools.Statistics(lambda ind: ind.fitness.values)
 	stats.register("avg", np.mean)
@@ -106,7 +147,9 @@
 	stats.register("max", np.max)
 
-	print('Evolution with population size %d for %d generations, optimization criteria: %s' % (POPSIZE, GENERATIONS, OPTIMIZATION_CRITERIA))
-	pop, log = algorithms.eaSimple(pop, toolbox, cxpb=0.2, mutpb=0.9, ngen=GENERATIONS, stats=stats, halloffame=hof, verbose=True)
+	pop, log = algorithms.eaSimple(pop, toolbox, cxpb=parsed_args.pxov, mutpb=parsed_args.pmut, ngen=parsed_args.generations, stats=stats, halloffame=hof, verbose=True)
 	print('Best individuals:')
-	for best in hof:
-		print(best.fitness, '\t-->\t', best[0])
+	for ind in hof:
+		print(ind.fitness, '\t-->\t', ind[0])
+
+	if parsed_args.hof_savefile is not None:
+		save_genotypes(parsed_args.hof_savefile, OPTIMIZATION_CRITERIA, hof)
Index: /framspy/run-deap-examples.cmd
===================================================================
--- /framspy/run-deap-examples.cmd	(revision 1161)
+++ /framspy/run-deap-examples.cmd	(revision 1161)
@@ -0,0 +1,23 @@
+rem To learn about all available options of the .py algorithm below, add "-h" to its parameters.
+rem Use the source code of the examples as a starting point for your customizations.
+rem Example usage:
+
+set DIR_WITH_FRAMS_LIBRARY=.........................
+
+
+
+
+rem simple one-criterion evolution, maximize the number of neurons
+python FramsticksEvolution.py   -path %DIR_WITH_FRAMS_LIBRARY%   -sim eval-allcriteria.sim  -opt numneurons
+ 
+
+rem as above but "chaining" .sim files, subsequent files overwrite selected parameters
+python FramsticksEvolution.py   -path %DIR_WITH_FRAMS_LIBRARY%   -sim eval-allcriteria.sim;deterministic.sim;sample-period-longest.sim    -opt velocity
+
+
+rem introducing hard limit on the number of Parts, using f9 genetic encoding and saving Hall of Fame
+python FramsticksEvolution.py   -path %DIR_WITH_FRAMS_LIBRARY%   -opt velocity   -max_numparts 6    -genformat 9   -hof_savefile HoF-f9.gen
+
+
+rem two criteria
+python FramsticksEvolution.py   -path %DIR_WITH_FRAMS_LIBRARY%   -popsize 40    -generations 10    -opt velocity,vertpos 
