Index: /framspy/evolalg_steps/base/frams_step.py
===================================================================
--- /framspy/evolalg_steps/base/frams_step.py	(revision 1185)
+++ /framspy/evolalg_steps/base/frams_step.py	(revision 1185)
@@ -0,0 +1,14 @@
+from evolalg_steps.base.step import Step
+
+
+class FramsStep(Step):
+    def __init__(self, frams_lib, commands=None, *args, **kwargs):
+        super(FramsStep, self).__init__(*args, **kwargs)
+        if commands is None:
+            commands = []
+        self.frams = frams_lib
+        self.commands = commands
+
+    def pre(self):
+        for c in self.commands:
+            self.frams.sendDirectCommand(c) #TODO update to use FramsticksLib when needed, pass lambda, import frams module? Maybe pre-lambda returns old value, post-lambda restores what pre-lambda returned. Or pass ExtValue or list of ExtValues and their new values (and either caller saves original values to be restored, or pre() returns a list of original values to be restored in post())
Index: /framspy/evolalg_steps/base/individual.py
===================================================================
--- /framspy/evolalg_steps/base/individual.py	(revision 1185)
+++ /framspy/evolalg_steps/base/individual.py	(revision 1185)
@@ -0,0 +1,5 @@
+
+
+class Individual:
+    def __init__(self, genotype):
+        self.genotype = genotype
Index: /framspy/evolalg_steps/base/lambda_step.py
===================================================================
--- /framspy/evolalg_steps/base/lambda_step.py	(revision 1185)
+++ /framspy/evolalg_steps/base/lambda_step.py	(revision 1185)
@@ -0,0 +1,25 @@
+from evolalg_steps.base.step import Step
+
+
+class LambdaStep(Step):
+    """
+    Wrapper for lambda expressions. In "normal" mode each step is given the entire population to work with. This class
+    helps with any Callable that accepts individuals.
+
+    """
+    def __init__(self, fun, in_place=True, *args, **kwargs):
+        """
+        @param fun: Callable callable that will be called for each individual
+        @param in_place: Bool
+        """
+        super(LambdaStep, self).__init__(*args, **kwargs)
+        self.fun = fun
+        self.in_place = in_place
+
+    def call(self, population):
+        super(LambdaStep, self).call(population)
+        if self.in_place:
+            [self.fun(_) for _ in population]
+        else:
+            population = [self.fun(_) for _ in population]
+        return population
Index: /framspy/evolalg_steps/base/step.py
===================================================================
--- /framspy/evolalg_steps/base/step.py	(revision 1185)
+++ /framspy/evolalg_steps/base/step.py	(revision 1185)
@@ -0,0 +1,34 @@
+import logging
+from abc import abstractmethod
+
+
+class Step:
+    """
+    Base abstract class for experiment's steps. It has three stages: pre, call and post.
+
+    """
+
+    def __init__(self, name=None):
+        self.name = name
+        if name is None:
+            self.name = type(self).__name__
+
+
+    def pre(self):
+        pass
+
+    @abstractmethod
+    def call(self, population, *args, **kwargs):
+        logging.getLogger(self.name).debug(f"Population size {len(population)}")
+
+    def post(self):
+        pass
+
+    def init(self):
+        pass
+
+    def __call__(self, *args, **kwargs):
+        self.pre()
+        res = self.call(*args, **kwargs)
+        self.post()
+        return res
Index: /framspy/evolalg_steps/base/union_step.py
===================================================================
--- /framspy/evolalg_steps/base/union_step.py	(revision 1185)
+++ /framspy/evolalg_steps/base/union_step.py	(revision 1185)
@@ -0,0 +1,29 @@
+from collections import Iterable
+
+from evolalg_steps.base.step import Step
+
+
+class UnionStep(Step):
+    def __init__(self, steps, *args, **kwargs):
+        super(UnionStep, self).__init__(*args, **kwargs)
+        if isinstance(steps, Iterable):
+            self.steps = steps
+        else:
+            self.steps = [steps]
+
+    def init(self):
+        for s in self.steps:
+            if isinstance(s, Step):
+                s.init()
+
+    def call(self, population):
+        super(UnionStep, self).call(population)
+        for s in self.steps:
+            population = s(population)
+        return population
+
+    def __len__(self):
+        return len(self.steps)
+
+    def __iter__(self):
+        return iter(self.steps)
Index: /framspy/evolalg_steps/dissimilarity/archive.py
===================================================================
--- /framspy/evolalg_steps/dissimilarity/archive.py	(revision 1185)
+++ /framspy/evolalg_steps/dissimilarity/archive.py	(revision 1185)
@@ -0,0 +1,36 @@
+import copy
+from typing import Union
+
+from evolalg_steps.base.step import Step
+from evolalg_steps.dissimilarity.dissimilarity import Dissimilarity
+from evolalg_steps.dissimilarity.frams_dissimilarity import FramsDissimilarity
+from evolalg_steps.statistics.halloffame_custom import HallOfFameCustom
+
+# TODO not fully tested. Verify if works OK, in particular test adding new individuals.
+class ArchiveDissimilarity(Step):
+
+    def __init__(self, archive_size, dissim: Union[Dissimilarity, FramsDissimilarity], order="max", field="dissim"):
+        self.name= "archive"
+        self.archive_size = archive_size
+        self.archive = []
+        self.dissim = dissim
+        self.order = order
+        self.field = field
+        if self.order not in ["min", "max"]:
+            raise ValueError("Order must be min or max")
+
+        if self.archive_size < 0:
+            raise ValueError(f"Archive size must be integer greater than or equal to 0. Got {self.archive_size}")
+
+    def call(self, population):
+        super(ArchiveDissimilarity, self).call(population)
+        population_archive = population + self.archive
+        population_archive = self.dissim(population_archive)
+
+        population = population_archive[:len(population)]
+        order = 1
+        if self.order == "max":
+            order *= -1
+        sorted_archive = sorted(population_archive, key=lambda x: getattr(x, self.field) * order)
+        self.archive = copy.deepcopy(sorted_archive[:self.archive_size])
+        return population
Index: /framspy/evolalg_steps/dissimilarity/dissimilarity.py
===================================================================
--- /framspy/evolalg_steps/dissimilarity/dissimilarity.py	(revision 1185)
+++ /framspy/evolalg_steps/dissimilarity/dissimilarity.py	(revision 1185)
@@ -0,0 +1,49 @@
+from abc import ABC
+
+from evolalg_steps.base.step import Step
+import numpy as np
+
+
+class Dissimilarity(Step, ABC):
+
+    def __init__(self, reduction="mean", output_field="dissim", knn=None, *args, **kwargs):
+        super(Dissimilarity, self).__init__(*args, **kwargs)
+
+        self.output_field = output_field
+        self.fn_reduce = Dissimilarity.get_reduction_by_name(reduction)
+        self.knn = knn
+
+
+    @staticmethod
+    def reduce(dissim_matrix, fn_reduce, knn):
+        if fn_reduce is None:
+            return dissim_matrix
+        elif fn_reduce is Dissimilarity.knn_mean:
+            return fn_reduce(dissim_matrix, 1, knn)
+        else:
+            return fn_reduce(dissim_matrix, axis=1)
+
+
+    @staticmethod
+    def knn_mean(dissim_matrix, axis, knn):
+        return np.mean(np.partition(dissim_matrix, knn)[:, :knn], axis=axis)
+
+
+    @staticmethod
+    def get_reduction_by_name(reduction: str):
+
+        if reduction not in REDUCTION_FUNCTION:
+            raise ValueError(f"Unknown reduction type '{reduction}'. Supported: {','.join(REDUCTION_FUNCTION.keys())}")
+
+        return REDUCTION_FUNCTION[reduction]
+
+
+
+REDUCTION_FUNCTION = {
+            "mean": np.mean,
+            "max": np.max,
+            "min": np.min,
+            "sum": np.sum,
+            "knn_mean": Dissimilarity.knn_mean,
+            "none": None
+        }
Index: /framspy/evolalg_steps/dissimilarity/frams_dissimilarity.py
===================================================================
--- /framspy/evolalg_steps/dissimilarity/frams_dissimilarity.py	(revision 1185)
+++ /framspy/evolalg_steps/dissimilarity/frams_dissimilarity.py	(revision 1185)
@@ -0,0 +1,27 @@
+from abc import ABC
+
+import numpy as np
+
+from evolalg_steps.base.frams_step import FramsStep
+from evolalg_steps.dissimilarity.dissimilarity import Dissimilarity
+
+
+class FramsDissimilarity(FramsStep):
+
+    def __init__(self, frams_lib, reduction="mean", output_field="dissim",knn=None, *args, **kwargs):
+        super(FramsDissimilarity, self).__init__(frams_lib, *args, **kwargs)
+
+        self.output_field = output_field
+        self.fn_reduce = Dissimilarity.get_reduction_by_name(reduction)
+        self.knn = knn
+
+
+    def call(self, population):
+        super(FramsDissimilarity, self).call(population)
+        if len(population) == 0:
+            return []
+        dissim_matrix = self.frams.dissimilarity([_.genotype for _ in population], 1)
+        dissim = Dissimilarity.reduce(dissim_matrix, self.fn_reduce, self.knn)
+        for d,ind in zip(dissim, population):
+            setattr(ind, self.output_field, d)
+        return population
Index: /framspy/evolalg_steps/dissimilarity/levenshtein.py
===================================================================
--- /framspy/evolalg_steps/dissimilarity/levenshtein.py	(revision 1185)
+++ /framspy/evolalg_steps/dissimilarity/levenshtein.py	(revision 1185)
@@ -0,0 +1,23 @@
+import Levenshtein as lev
+
+from evolalg_steps.dissimilarity.dissimilarity import Dissimilarity
+
+
+class LevenshteinDissimilarity(Dissimilarity):
+    def __init__(self, reduction="mean", output_field="dissim", *args, **kwargs):
+        super(LevenshteinDissimilarity, self).__init__(reduction, output_field, *args, **kwargs)
+
+    def call(self, population):
+        super(LevenshteinDissimilarity, self).call(population)
+        if len(population) == 0:
+            return []
+        dissim = []
+        for i, p in enumerate(population):
+            gen_dis = []
+            for i2, p2 in enumerate(population):
+                gen_dis.append(lev.distance(p.genotype, p2.genotype))
+            dissim.append(gen_dis)
+        dissim = self.reduce(dissim, self.fn_reduce, self.knn)
+        for d, ind in zip(dissim, population):
+            setattr(ind, self.output_field, d)
+        return population
Index: /framspy/evolalg_steps/examples/multicriteria.py
===================================================================
--- /framspy/evolalg_steps/examples/multicriteria.py	(revision 1185)
+++ /framspy/evolalg_steps/examples/multicriteria.py	(revision 1185)
@@ -0,0 +1,336 @@
+#TODO hof should be the complete non-dominated set from the entire process of evolution (all evaluated individuals), not limited in any way (remove '-hof_size'). Now we are not storing the entire Pareto front, but individuals that were better than others in hof [better=on all criteria?] at the time of inserting to hof. Hof has a size limit.
+#TODO when -dissim is used, print its statistics just like all other criteria
+#TODO in statistics, do not print "gen" (generation number) for each criterion
+#TODO if possible, when saving the final .gen file, instead of fitness as a python tuple, save individual criteria - so instead of fitness:(0.005251036058321138, 0.025849976588613266), write "velocity:...\nvertpos:..." (this also applies to other .py examples in this directory)
+
+
+import argparse
+import logging
+import os
+import pickle
+import sys
+import copy
+from enum import Enum
+
+import numpy as np
+
+from deap import base
+from deap import tools
+
+from FramsticksLib import FramsticksLib
+from evolalg_steps.base.lambda_step import LambdaStep
+from evolalg_steps.base.step import Step
+from evolalg_steps.dissimilarity.frams_dissimilarity import FramsDissimilarity
+from evolalg_steps.dissimilarity.levenshtein import LevenshteinDissimilarity
+from evolalg_steps.experiment import Experiment
+from evolalg_steps.fitness.fitness_step import FitnessStep
+from evolalg_steps.mutation_cross.frams_cross_and_mutate import FramsCrossAndMutate
+from evolalg_steps.population.frams_population import FramsPopulation
+from evolalg_steps.repair.remove.field import FieldRemove
+from evolalg_steps.repair.remove.remove import Remove
+from evolalg_steps.selection.nsga2 import NSGA2Selection
+from evolalg_steps.statistics.halloffame_stats import HallOfFameStatistics
+from evolalg_steps.statistics.multistatistics_deap import MultiStatistics
+from evolalg_steps.statistics.statistics_deap import StatisticsDeap
+from evolalg_steps.base.union_step import UnionStep
+from evolalg_steps.utils.population_save import PopulationSave
+
+
+def ensureDir(string):
+    if os.path.isdir(string):
+        return string
+    else:
+        raise NotADirectoryError(string)
+
+
+class Dissim(Enum):
+    levenshtein = "levenshtein"
+    frams = "frams"
+
+    def __str__(self):
+        return self.name
+
+
+
+def parseArguments():
+    parser = argparse.ArgumentParser(
+        description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[
+            0])
+    parser.add_argument('-path', type=ensureDir, required=True,
+                        help='Path to the Framsticks library without trailing slash.')
+    parser.add_argument('-opt', required=True,
+                        help='optimization criteria seperated with a comma: vertpos, velocity, distance, vertvel, lifespan, numjoints, numparts, numneurons, numconnections (and others as long as they are provided by the .sim file and its .expdef). Single or multiple criteria.')
+    parser.add_argument('-lib', required=False, help="Filename of .so or .dll with the Framsticks library")
+
+    parser.add_argument('-genformat', required=False, default="1",
+                        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="Name of the .sim file with all parameter values. If you want to provide more files, separate them with a semicolon ';'.")
+    parser.add_argument('-dissim', required=False, type=Dissim, default=Dissim.frams,
+                        help='Dissimilarity measure, default: frams', choices=list(Dissim))
+    parser.add_argument('-popsize', type=int, default=40, help="Population size (must be a multiple of 4), default: 40.") # mod 4 because of DEAP
+    parser.add_argument('-generations', type=int, default=5, help="Number of generations, default: 5.")
+
+    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('-hof_size', type=int, default=10, help="Number of genotypes in Hall of Fame. Default: 10.")
+    parser.add_argument('-hof_evaluations', type=int, default=20,
+                        help="Number of final evaluations of each genotype in Hall of Fame to obtain reliable (averaged) fitness. Default: 20.")
+    parser.add_argument('-checkpoint_path', required=False, default=None, help="Path to the checkpoint file")
+    parser.add_argument('-checkpoint_interval', required=False, type=int, default=100, help="Checkpoint interval")
+    parser.add_argument('-debug', dest='debug', action='store_true', help="Prints names of steps as they are executed")
+    parser.set_defaults(debug=False)
+    return parser.parse_args()
+
+
+class NumPartsHigher(Remove):
+    def __init__(self, max_number):
+        super(NumPartsHigher, self).__init__()
+        self.max_number = max_number
+
+    def remove(self, individual):
+        return individual.numparts > self.max_number
+
+
+class NumJointsHigher(Remove):
+    def __init__(self, max_number):
+        super(NumJointsHigher, self).__init__()
+        self.max_number = max_number
+
+    def remove(self, individual):
+        return individual.numjoints > self.max_number
+
+
+class NumNeuronsHigher(Remove):
+    def __init__(self, max_number):
+        super(NumNeuronsHigher, self).__init__()
+        self.max_number = max_number
+
+    def remove(self, individual):
+        return individual.numneurons > self.max_number
+
+
+class NumConnectionsHigher(Remove):
+    def __init__(self, max_number):
+        super(NumConnectionsHigher, self).__init__()
+        self.max_number = max_number
+
+    def remove(self, individual):
+        return individual.numconnections > self.max_number
+
+
+class ReplaceWithHallOfFame(Step):
+    def __init__(self, hof, *args, **kwargs):
+        super(ReplaceWithHallOfFame, self).__init__(*args, **kwargs)
+        self.hof = hof
+
+    def call(self, population, *args, **kwargs):
+        super(ReplaceWithHallOfFame, self).call(population)
+        return list(self.hof.halloffame)
+
+
+class DeapFitness(base.Fitness):
+    weights = (1, 1)
+
+    def __init__(self, *args, **kwargs):
+        super(DeapFitness, self).__init__(*args, **kwargs)
+
+
+class Nsga2Fitness:
+    def __init__(self, fields):
+        self.fields = fields
+    def __call__(self, ind):
+        setattr(ind, "fitness", DeapFitness(tuple(getattr(ind, _) for _ in self.fields)))
+
+
+
+class ExtractField:
+    def __init__(self, field_name):
+        self.field_name = field_name
+    def __call__(self, ind):
+        return getattr(ind, self.field_name)
+
+def extract_fitness(ind):
+    return ind.fitness_raw
+
+
+def load_experiment(path):
+    with open(path, "rb") as file:
+        experiment = pickle.load(file)
+    print("Loaded experiment. Generation:", experiment.generation)
+    return experiment
+
+
+def create_experiment():
+    parsed_args = parseArguments()
+    frams_lib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.sim.split(";"))
+
+    opt_dissim = []
+    opt_fitness = []
+    for crit in parsed_args.opt.split(','):
+        try:
+            Dissim(crit)
+            opt_dissim.append(crit)
+        except ValueError:
+            opt_fitness.append(crit)
+    if len(opt_dissim) > 1:
+        raise ValueError("Only one type of dissimilarity supported")
+
+    # Steps for generating first population
+    init_stages = [
+        FramsPopulation(frams_lib, parsed_args.genformat, parsed_args.popsize)
+    ]
+
+    # Selection procedure
+    selection = NSGA2Selection(copy=True)
+
+    # Procedure for generating new population. This steps will be run as long there is less than
+    # popsize individuals in the new population
+    new_generation_stages = [FramsCrossAndMutate(frams_lib, cross_prob=0.2, mutate_prob=0.9)]
+
+    # Steps after new population is created. Executed exactly once per generation.
+    generation_modifications = []
+
+    # -------------------------------------------------
+    # Fitness
+
+    fitness_raw = FitnessStep(frams_lib, fields={**{_:_ for _ in opt_fitness},
+                                                 "numparts": "numparts",
+                                                 "numjoints": "numjoints",
+                                                 "numneurons": "numneurons",
+                                                 "numconnections": "numconnections"},
+                              fields_defaults={parsed_args.opt: None, "numparts": float("inf"),
+                                               "numjoints": float("inf"), "numneurons": float("inf"),
+                                               "numconnections": float("inf"),
+                                               **{_:None for _ in opt_fitness}
+                                               },
+                              evaluation_count=1)
+
+    fitness_end = FitnessStep(frams_lib, fields={_:_ for _ in opt_fitness},
+                              fields_defaults={parsed_args.opt: None},
+                              evaluation_count=parsed_args.hof_evaluations)
+    # Remove
+    remove = []
+    remove.append(FieldRemove(opt_fitness[0], None))  # Remove individuals if they have default value for fitness
+    if parsed_args.max_numparts is not None:
+        # This could be also implemented by "LambdaRemove(lambda x: x.numparts > parsed_args.num_parts)"
+        # But this would not serialize in checkpoint.
+        remove.append(NumPartsHigher(parsed_args.max_numparts))
+    if parsed_args.max_numjoints is not None:
+        remove.append(NumJointsHigher(parsed_args.max_numjoints))
+    if parsed_args.max_numneurons is not None:
+        remove.append(NumNeuronsHigher(parsed_args.max_numneurons))
+    if parsed_args.max_numconnections is not None:
+        remove.append(NumConnectionsHigher(parsed_args.max_numconnections))
+
+    remove_step = UnionStep(remove)
+
+    fitness_remove = UnionStep([fitness_raw, remove_step])
+
+    init_stages.append(fitness_remove)
+    new_generation_stages.append(fitness_remove)
+
+    # -------------------------------------------------
+    # Dissimilarity as one of the criteria
+    dissim = None
+    if len(opt_dissim) > 0 and Dissim(opt_dissim[0]) == Dissim.levenshtein:
+        dissim = LevenshteinDissimilarity(reduction="mean", output_field="dissim")
+    elif len(opt_dissim) > 0 and Dissim(opt_dissim[0]) == Dissim.frams:
+        dissim = FramsDissimilarity(frams_lib, reduction="mean", output_field="dissim")
+
+    if dissim is not None:
+        init_stages.append(dissim)
+        generation_modifications.append(dissim)
+
+    if dissim is not None:
+        nsga2_fittnes = Nsga2Fitness(["dissim"]+ opt_fitness)
+    else:
+        nsga2_fittnes = Nsga2Fitness(opt_fitness)
+
+    init_stages.append(LambdaStep(nsga2_fittnes))
+    generation_modifications.append(LambdaStep(nsga2_fittnes))
+
+    # -------------------------------------------------
+    # Statistics
+    hall_of_fame = HallOfFameStatistics(parsed_args.hof_size, "fitness")  # Wrapper for halloffamae
+    replace_with_hof = ReplaceWithHallOfFame(hall_of_fame)
+    statistics_deap = MultiStatistics({fit:StatisticsDeap([
+        ("avg", np.mean),
+        ("stddev", np.std),
+        ("min", np.min),
+        ("max", np.max)
+    ], ExtractField(fit)) for fit in opt_fitness}) # Wrapper for deap statistics
+
+    statistics_union = UnionStep(
+        [hall_of_fame,
+        statistics_deap]
+    )  # Union of two statistics steps.
+
+    init_stages.append(statistics_union)
+    generation_modifications.append(statistics_union)
+
+    # -------------------------------------------------
+    # End stages: this will execute exactly once after all generations.
+    end_stages = [
+        replace_with_hof,
+        fitness_end,
+        PopulationSave("halloffame.gen", provider=hall_of_fame.halloffame, fields={"genotype": "genotype",
+                                                                                   "fitness": "fitness"})]
+    # ...but custom fields can be added, e.g. "custom": "recording"
+
+    # -------------------------------------------------
+
+    # Experiment creation
+
+    experiment = Experiment(init_population=init_stages,
+                            selection=selection,
+                            new_generation_steps=new_generation_stages,
+                            generation_modification=generation_modifications,
+                            end_steps=end_stages,
+                            population_size=parsed_args.popsize,
+                            checkpoint_path=parsed_args.checkpoint_path,
+                            checkpoint_interval=parsed_args.checkpoint_interval
+                            )
+    return experiment
+
+
+def main():
+    print("Running experiment with", sys.argv)
+    parsed_args = parseArguments()
+    
+    if parsed_args.popsize % 4 != 0:
+    	raise ValueError("popsize must be a multiple of 4 (for example %d)." % (parsed_args.popsize//4*4)) # required by deap.tools.selTournamentDCD() 
+    	
+    if parsed_args.debug:
+        logging.basicConfig(level=logging.DEBUG)
+
+    if parsed_args.checkpoint_path is not None and os.path.exists(parsed_args.checkpoint_path):
+        experiment = load_experiment(parsed_args.checkpoint_path)
+    else:
+        experiment = create_experiment()
+        experiment.init()  # init is mandatory
+
+    experiment.run(parsed_args.generations)
+
+    # Next call for experiment.run(10) will do nothing. Parameter 10 specifies how many generations should be
+    # in one experiment. Previous call generated 10 generations.
+    # Example 1:
+    # experiment.init()
+    # experiment.run(10)
+    # experiment.run(12)
+    # #This will run for total of 12 generations
+    #
+    # Example 2
+    # experiment.init()
+    # experiment.run(10)
+    # experiment.init()
+    # experiment.run(10)
+    # # All work produced by first run will be "destroyed" by second init().
+
+
+if __name__ == '__main__':
+    main()
Index: /framspy/evolalg_steps/examples/niching_novelty.py
===================================================================
--- /framspy/evolalg_steps/examples/niching_novelty.py	(revision 1185)
+++ /framspy/evolalg_steps/examples/niching_novelty.py	(revision 1185)
@@ -0,0 +1,370 @@
+import argparse
+import logging
+import os
+import pickle
+import sys
+from enum import Enum
+
+import numpy as np
+
+from FramsticksLib import FramsticksLib
+from evolalg_steps.base.lambda_step import LambdaStep
+from evolalg_steps.base.step import Step
+from evolalg_steps.dissimilarity.archive import ArchiveDissimilarity
+from evolalg_steps.dissimilarity.frams_dissimilarity import FramsDissimilarity
+from evolalg_steps.dissimilarity.levenshtein import LevenshteinDissimilarity
+from evolalg_steps.experiment import Experiment
+from evolalg_steps.fitness.fitness_step import FitnessStep
+from evolalg_steps.mutation_cross.frams_cross_and_mutate import FramsCrossAndMutate
+from evolalg_steps.population.frams_population import FramsPopulation
+from evolalg_steps.repair.remove.field import FieldRemove
+from evolalg_steps.repair.remove.remove import Remove
+from evolalg_steps.selection.tournament import TournamentSelection
+from evolalg_steps.statistics.halloffame_stats import HallOfFameStatistics
+from evolalg_steps.statistics.statistics_deap import StatisticsDeap
+from evolalg_steps.base.union_step import UnionStep
+from evolalg_steps.utils.population_save import PopulationSave
+
+
+def ensureDir(string):
+    if os.path.isdir(string):
+        return string
+    else:
+        raise NotADirectoryError(string)
+
+
+class Dissim(Enum):
+    levenshtein = "levenshtein"
+    frams = "frams"
+
+    def __str__(self):
+        return self.name
+
+
+class Fitness(Enum):
+    raw = "raw"
+    niching = "niching"
+    novelty = "novelty"
+    knn_niching = "knn_niching"
+    knn_novelty = "knn_novelty"
+
+    def __str__(self):
+        return self.name
+
+
+def parseArguments():
+    parser = argparse.ArgumentParser(
+        description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[
+            0])
+    parser.add_argument('-path', type=ensureDir, required=True,
+                        help='Path to the Framsticks library without trailing slash.')
+    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, see multicriteria.py.')
+    parser.add_argument('-lib', required=False, help="Filename of .so or .dll with the Framsticks library")
+
+    parser.add_argument('-genformat', required=False, default="1",
+                        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="Name of the .sim file with all parameter values. If you want to provide more files, separate them with a semicolon ';'.")
+    parser.add_argument('-fit', required=False, default=Fitness.raw, type=Fitness,
+                        help=' Fitness criteria, default: raw', choices=list(Fitness))
+    parser.add_argument('-dissim', required=False, type=Dissim, default=Dissim.frams,
+                        help='Dissimilarity measure, default: frams', choices=list(Dissim))
+    parser.add_argument('-knn', type=int,
+                        help="'k' value for knn-based fitness criteria (knn-niching and knn-novelty).")
+    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('-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=10000, help="Maximum number of characters in genotype, to disable this option set it to -1. Default: 10 000")
+    parser.add_argument('-hof_size', type=int, default=10, help="Number of genotypes in Hall of Fame. Default: 10.")
+    parser.add_argument('-hof_evaluations', type=int, default=20,
+                        help="Number of final evaluations of each genotype in Hall of Fame to obtain reliable (averaged) fitness. Default: 20.")
+    parser.add_argument('-checkpoint_path', required=False, default=None, help="Path to the checkpoint file")
+    parser.add_argument('-checkpoint_interval', required=False, type=int, default=100, help="Checkpoint interval")
+    parser.add_argument('-debug', dest='debug', action='store_true', help="Prints names of steps as they are executed")
+    parser.add_argument('-archive_size', type=int, default=0, help="Size of the archive size for dissimilarity calculation")
+    parser.set_defaults(debug=False)
+    return parser.parse_args()
+
+
+def extract_fitness(ind):
+    return ind.fitness_raw
+
+
+def print_population_count(pop):
+    print("Current popsize:", len(pop))
+    return pop  # Each step must return a population
+
+
+class NumPartsHigher(Remove):
+    def __init__(self, max_number):
+        super(NumPartsHigher, self).__init__()
+        self.max_number = max_number
+
+    def remove(self, individual):
+        return individual.numparts > self.max_number
+
+
+class NumJointsHigher(Remove):
+    def __init__(self, max_number):
+        super(NumJointsHigher, self).__init__()
+        self.max_number = max_number
+
+    def remove(self, individual):
+        return individual.numjoints > self.max_number
+
+
+class NumNeuronsHigher(Remove):
+    def __init__(self, max_number):
+        super(NumNeuronsHigher, self).__init__()
+        self.max_number = max_number
+
+    def remove(self, individual):
+        return individual.numneurons > self.max_number
+
+
+class NumConnectionsHigher(Remove):
+    def __init__(self, max_number):
+        super(NumConnectionsHigher, self).__init__()
+        self.max_number = max_number
+
+    def remove(self, individual):
+        return individual.numconnections > self.max_number
+
+class NumCharsHigher(Remove):
+    def __init__(self, max_number):
+        super(NumCharsHigher, self).__init__()
+        self.max_number = max_number
+
+    def remove(self, individual):
+        return len(individual.genotype) > self.max_number
+
+class ReplaceWithHallOfFame(Step):
+    def __init__(self, hof, *args, **kwargs):
+        super(ReplaceWithHallOfFame, self).__init__(*args, **kwargs)
+        self.hof = hof
+
+    def call(self, population, *args, **kwargs):
+        super(ReplaceWithHallOfFame, self).call(population)
+        return list(self.hof.halloffame)
+
+
+def func_raw(ind): setattr(ind, "fitness", ind.fitness_raw)
+
+
+def func_novelty(ind): setattr(ind, "fitness", ind.dissim)
+
+
+def func_knn_novelty(ind): setattr(ind, "fitness", ind.dissim)
+
+
+def func_niching(ind): setattr(ind, "fitness", ind.fitness_raw * (1 + ind.dissim))
+
+
+def func_knn_niching(ind): setattr(ind, "fitness", ind.fitness_raw * (1 + ind.dissim))
+
+
+def load_experiment(path):
+    with open(path, "rb") as file:
+        experiment = pickle.load(file)
+    print("Loaded experiment. Generation:", experiment.generation)
+    return experiment
+
+
+def create_experiment():
+    parsed_args = parseArguments()
+    frams_lib = FramsticksLib(parsed_args.path, parsed_args.lib,
+                              parsed_args.sim.split(";"))
+    # Steps for generating first population
+    init_stages = [
+        FramsPopulation(frams_lib, parsed_args.genformat, parsed_args.popsize)
+    ]
+
+    # Selection procedure
+    selection = TournamentSelection(parsed_args.tournament,
+                                    copy=True)  # 'fitness' by default, the targeted attribute can be changed, e.g. fit_attr="fitness_raw"
+
+    # Procedure for generating new population. This steps will be run as long there is less than
+    # popsize individuals in the new population
+    new_generation_stages = [FramsCrossAndMutate(frams_lib, cross_prob=0.2, mutate_prob=0.9)]
+
+    # Steps after new population is created. Executed exactly once per generation.
+    generation_modifications = []
+
+    # -------------------------------------------------
+    # Fitness
+
+    fitness_raw = FitnessStep(frams_lib, fields={parsed_args.opt: "fitness_raw",
+                                                 "numparts": "numparts",
+                                                 "numjoints": "numjoints",
+                                                 "numneurons": "numneurons",
+                                                 "numconnections": "numconnections"},
+                              fields_defaults={parsed_args.opt: None, "numparts": float("inf"),
+                                               "numjoints": float("inf"), "numneurons": float("inf"),
+                                               "numconnections": float("inf")},
+                              evaluation_count=1)
+
+    fitness_end = FitnessStep(frams_lib, fields={parsed_args.opt: "fitness_raw"},
+                              fields_defaults={parsed_args.opt: None},
+                              evaluation_count=parsed_args.hof_evaluations)
+    # Remove
+    remove = []
+    remove.append(FieldRemove("fitness_raw", None))  # Remove individuals if they have default value for fitness
+    if parsed_args.max_numparts is not None:
+        # This could be also implemented by "LambdaRemove(lambda x: x.numparts > parsed_args.num_parts)"
+        # But this would not serialize in checkpoint.
+        remove.append(NumPartsHigher(parsed_args.max_numparts))
+    if parsed_args.max_numjoints is not None:
+        remove.append(NumJointsHigher(parsed_args.max_numjoints))
+    if parsed_args.max_numneurons is not None:
+        remove.append(NumNeuronsHigher(parsed_args.max_numneurons))
+    if parsed_args.max_numconnections is not None:
+        remove.append(NumConnectionsHigher(parsed_args.max_numconnections))
+    if parsed_args.max_numgenochars != -1:
+        remove.append(NumCharsHigher(parsed_args.max_numgenochars))
+
+    remove_step = UnionStep(remove)
+
+    fitness_remove = UnionStep([fitness_raw, remove_step])
+
+    init_stages.append(fitness_remove)
+    new_generation_stages.append(fitness_remove)
+
+    # -------------------------------------------------
+    # Novelty or niching
+    knn = parsed_args.knn
+    if parsed_args.fit == Fitness.knn_novelty or parsed_args.fit == Fitness.knn_niching:
+        reduction_method = "knn_mean"
+        assert knn is not None, "'k' must be set for knn-based fitness."
+        assert knn > 0, "'k' must be positive."
+        assert knn < parsed_args.popsize, "'k' must be smaller than population size."
+    else:
+        reduction_method = "mean"
+        assert knn is None, "'k' is irrelevant unless knn-based fitness is used."
+
+    dissim = None
+    if parsed_args.dissim == Dissim.levenshtein:
+        dissim = LevenshteinDissimilarity(reduction=reduction_method, knn=knn, output_field="dissim")
+    elif parsed_args.dissim == Dissim.frams:
+        dissim = FramsDissimilarity(frams_lib, reduction=reduction_method, knn=knn, output_field="dissim")
+
+    if parsed_args.fit == Fitness.raw:
+        # Fitness is equal to finess raw
+        raw = LambdaStep(func_raw)
+        init_stages.append(raw)
+        generation_modifications.append(raw)
+
+    if parsed_args.fit == Fitness.niching:  # TODO reduce redundancy in the four cases below: dictionary?
+
+        niching = UnionStep([
+            ArchiveDissimilarity(parsed_args.archive_size, dissim),
+            LambdaStep(func_niching)
+        ])
+        init_stages.append(niching)
+        generation_modifications.append(niching)
+
+    if parsed_args.fit == Fitness.novelty:
+        novelty = UnionStep([
+            ArchiveDissimilarity(parsed_args.archive_size, dissim),
+            LambdaStep(func_novelty)
+        ])
+        init_stages.append(novelty)
+        generation_modifications.append(novelty)
+
+    if parsed_args.fit == Fitness.knn_niching:
+        knn_niching = UnionStep([
+            ArchiveDissimilarity(parsed_args.archive_size, dissim),
+            LambdaStep(func_knn_niching)
+        ])
+        init_stages.append(knn_niching)
+        generation_modifications.append(knn_niching)
+
+    if parsed_args.fit == Fitness.knn_novelty:
+        knn_novelty = UnionStep([
+            ArchiveDissimilarity(parsed_args.archive_size, dissim),
+            LambdaStep(func_knn_novelty)
+        ])
+        init_stages.append(knn_novelty)
+        generation_modifications.append(knn_novelty)
+
+    # -------------------------------------------------
+    # Statistics
+    hall_of_fame = HallOfFameStatistics(parsed_args.hof_size, "fitness_raw")  # Wrapper for halloffamae
+    replace_with_hof = ReplaceWithHallOfFame(hall_of_fame)
+    statistics_deap = StatisticsDeap([
+        ("avg", np.mean),
+        ("stddev", np.std),
+        ("min", np.min),
+        ("max", np.max)
+    ], extract_fitness)  # Wrapper for deap statistics
+
+    statistics_union = UnionStep([
+        hall_of_fame,
+        statistics_deap
+    ])  # Union of two statistics steps.
+
+    init_stages.append(statistics_union)
+    generation_modifications.append(statistics_union)
+
+    # -------------------------------------------------
+    # End stages: this will execute exactly once after all generations.
+    end_stages = [
+        replace_with_hof,
+        fitness_end,
+        PopulationSave("halloffame.gen", provider=hall_of_fame.halloffame, fields={"genotype": "genotype",
+                                                                                   "fitness": "fitness_raw"})]
+    # ...but custom fields can be added, e.g. "custom": "recording"
+
+    # -------------------------------------------------
+
+    # Experiment creation
+
+    experiment = Experiment(init_population=init_stages,
+                            selection=selection,
+                            new_generation_steps=new_generation_stages,
+                            generation_modification=generation_modifications,
+                            end_steps=end_stages,
+                            population_size=parsed_args.popsize,
+                            checkpoint_path=parsed_args.checkpoint_path,
+                            checkpoint_interval=parsed_args.checkpoint_interval
+                            )
+    return experiment
+
+
+def main():
+    print("Running experiment with", sys.argv)
+    parsed_args = parseArguments()
+    if parsed_args.debug:
+        logging.basicConfig(level=logging.DEBUG)
+
+    if parsed_args.checkpoint_path is not None and os.path.exists(parsed_args.checkpoint_path):
+        experiment = load_experiment(parsed_args.checkpoint_path)
+    else:
+        experiment = create_experiment()
+        experiment.init()  # init is mandatory
+
+    experiment.run(parsed_args.generations)
+
+    # Next call for experiment.run(10) will do nothing. Parameter 10 specifies how many generations should be
+    # in one experiment. Previous call generated 10 generations.
+    # Example 1:
+    # experiment.init()
+    # experiment.run(10)
+    # experiment.run(12)
+    # #This will run for total of 12 generations
+    #
+    # Example 2
+    # experiment.init()
+    # experiment.run(10)
+    # experiment.init()
+    # experiment.run(10)
+    # # All work produced by first run will be "destroyed" by second init().
+
+
+if __name__ == '__main__':
+    main()
Index: /framspy/evolalg_steps/examples/standard.py
===================================================================
--- /framspy/evolalg_steps/examples/standard.py	(revision 1185)
+++ /framspy/evolalg_steps/examples/standard.py	(revision 1185)
@@ -0,0 +1,129 @@
+import argparse
+import os
+import sys
+import numpy as np
+
+# TODO add new example: steadystate.py (analogous to standard.py) OR include steadysteate as a mode in this example or in niching_novelty.py
+# TODO extend both standard.py and steadystate.py to support >1 criteria (using DEAP's selNSGA2() and selSPEA2())
+# TODO add comments to all examples in this directory
+# TODO add to standard.py and steadystate.py evaluating each genotype in HOF N (configurable, default 20) times when the evolution ends, as it is in niching_novelty.py
+# TODO "-debug" mode, indent nested steps (pre++, post-- of a static counter?) and print their arguments so it is easy to see what happens during evolution
+
+
+from FramsticksLib import FramsticksLib
+from evolalg_steps.base.union_step import UnionStep
+from evolalg_steps.experiment import Experiment
+from evolalg_steps.fitness.fitness_step import FitnessStep
+from evolalg_steps.mutation_cross.frams_cross_and_mutate import FramsCrossAndMutate
+from evolalg_steps.population.frams_population import FramsPopulation
+from evolalg_steps.repair.remove.field import FieldRemove
+from evolalg_steps.selection.tournament import TournamentSelection
+from evolalg_steps.statistics.halloffame_stats import HallOfFameStatistics
+from evolalg_steps.statistics.statistics_deap import StatisticsDeap
+from evolalg_steps.utils.population_save import PopulationSave
+
+
+
+EVAL_LIFESPAN_BEHAVIOR = False  # if False, standard evaluation criteria can be used as fitness as defined by the -opt parameter. If True, it is assumed that the expdef provides custom dictionary fields in evaluation, and they need to be handled specifically in python source code below (this could be parametrized in command-line too, but the syntax would be complex).
+
+
+def ensureDir(string):
+    if os.path.isdir(string):
+        return string
+    else:
+        raise NotADirectoryError(string)
+
+
+def parseArguments():
+    parser = argparse.ArgumentParser(
+        description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[
+            0])
+    parser.add_argument('-path', type=ensureDir, required=True, help='Path to the Framsticks library without trailing slash.')
+    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). Single or multiple criteria.')
+    parser.add_argument('-lib', required=False, help="Filename of .so or .dll with the Framsticks library")
+
+    parser.add_argument('-genformat', required=False, default="1",
+                        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="Name of the .sim file with all parameter values. If you want to provide more files, separate them with a semicolon ';'.")
+    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('-hof_size', type=int, default=10, help="Number of genotypes in Hall of Fame. Default: 10.")
+    return parser.parse_args()
+
+
+def extract_fitness(ind):
+    return ind.fitness
+
+
+def print_population_count(pop):
+    print("Current popsize:", len(pop))
+    return pop  # Each step must return a population
+
+
+def main():
+    parsed_args = parseArguments()
+    frams_lib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.sim.split(";"))
+
+    hall_of_fame = HallOfFameStatistics(parsed_args.hof_size, "fitness")
+    statistics_union = UnionStep([
+        hall_of_fame,
+        StatisticsDeap([
+            ("avg", np.mean),
+            ("stddev", np.std),
+            ("min", np.min),
+            ("max", np.max),
+            ("count", len)
+        ], extract_fitness)
+    ])
+
+    fitness_remove = UnionStep(  # evaluate performance and fitness, rename some of the fields, and remove some performance fields that we get from Framsticks, but we don't need them here
+        [
+        FitnessStep(frams_lib, fields={"velocity": "fitness", "data->recording": "recording"},
+                    fields_defaults={"velocity": None, "data->recording": None})  # custom definitions and handling
+        if EVAL_LIFESPAN_BEHAVIOR else
+        FitnessStep(frams_lib, fields={parsed_args.opt: "fitness"}, fields_defaults={parsed_args.opt: None})
+        ]
+        +
+        ([FieldRemove("recording", None)] if EVAL_LIFESPAN_BEHAVIOR else [FieldRemove("fitness", None)])
+        +
+        [print_population_count]  # Stages can also be any Callable
+    )
+
+    selection = TournamentSelection(parsed_args.tournament, copy=True, fit_attr="fitness")
+    new_generation_steps = [
+        FramsCrossAndMutate(frams_lib, cross_prob=0.2, mutate_prob=0.9),
+        fitness_remove
+    ]
+
+    generation_modifications = [
+        statistics_union  # Or niching, novelty
+    ]
+
+    init_stages = [FramsPopulation(frams_lib, parsed_args.genformat, parsed_args.popsize),
+                   fitness_remove,  # It is possible to create smaller population
+                   statistics_union]
+
+    end_steps = [PopulationSave("halloffame.gen", provider=hall_of_fame.halloffame,
+                                fields={"genotype": "genotype", "fitness": "fitness", "custom": "recording"}
+                                if EVAL_LIFESPAN_BEHAVIOR
+                                else {"genotype": "genotype", "fitness": "fitness"}
+                                )]
+
+    experiment = Experiment(init_population=init_stages,
+                            selection=selection,
+                            new_generation_steps=new_generation_steps,
+                            generation_modification=generation_modifications,
+                            end_steps=end_steps,
+                            population_size=parsed_args.popsize
+                            )
+    experiment.init()
+    experiment.run(parsed_args.generations)
+    for ind in hall_of_fame.halloffame:
+        print("%g\t%s" % (ind.fitness, ind.genotype))
+
+
+if __name__ == '__main__':
+    main()
Index: /framspy/evolalg_steps/experiment.py
===================================================================
--- /framspy/evolalg_steps/experiment.py	(revision 1185)
+++ /framspy/evolalg_steps/experiment.py	(revision 1185)
@@ -0,0 +1,79 @@
+import os
+from typing import List, Callable, Union
+
+from evolalg_steps.base.step import Step
+import pickle
+import time
+
+from evolalg_steps.base.union_step import UnionStep
+from evolalg_steps.selection.selection import Selection
+from evolalg_steps.utils.stable_generation import StableGeneration
+import logging
+
+class Experiment:
+    def __init__(self, init_population: List[Callable],
+                 selection: Selection,
+                 new_generation_steps: List[Union[Callable, Step]],
+                 generation_modification: List[Union[Callable, Step]],
+                 end_steps: List[Union[Callable, Step]],
+                 population_size,
+                 checkpoint_path=None, checkpoint_interval=None):
+
+        self.init_population = init_population
+        self.running_time = 0
+        self.step = StableGeneration(
+            selection=selection,
+            steps=new_generation_steps,
+            population_size=population_size)
+        self.generation_modification = UnionStep(generation_modification)
+
+        self.end_steps = UnionStep(end_steps)
+
+        self.checkpoint_path = checkpoint_path
+        self.checkpoint_interval = checkpoint_interval
+        self.generation = 0
+        self.population = None
+
+    def init(self):
+        self.generation = 0
+        for s in self.init_population:
+            if isinstance(s, Step):
+                s.init()
+
+        self.step.init()
+        self.generation_modification.init()
+        self.end_steps.init()
+        self.population = []
+        for s in self.init_population:
+            self.population = s(self.population)
+
+    def run(self, num_generations):
+        for i in range(self.generation + 1, num_generations + 1):
+            start_time = time.time()
+            self.generation = i
+            self.population = self.step(self.population)
+            self.population = self.generation_modification(self.population)
+
+            self.running_time += time.time() - start_time
+            if (self.checkpoint_path is not None
+                    and self.checkpoint_interval is not None
+                    and i % self.checkpoint_interval == 0):
+                self.save_checkpoint()
+
+        self.population = self.end_steps(self.population)
+
+    def save_checkpoint(self):
+        tmp_filepath = self.checkpoint_path+"_tmp"
+        try:
+            with open(tmp_filepath, "wb") as file:
+                pickle.dump(self, file)
+            os.replace(tmp_filepath, self.checkpoint_path)  # ensures the new file was first saved OK (e.g. enough free space on device), then replace
+        except Exception as ex:
+            raise RuntimeError("Failed to save checkpoint '%s' (because: %s). This does not prevent the experiment from continuing, but let's stop here to fix the problem with saving checkpoints." % (tmp_filepath, ex))
+
+
+    @staticmethod
+    def restore(path):
+        with open(path) as file:
+            res = pickle.load(file)
+        return res
Index: /framspy/evolalg_steps/fitness/fitness_step.py
===================================================================
--- /framspy/evolalg_steps/fitness/fitness_step.py	(revision 1185)
+++ /framspy/evolalg_steps/fitness/fitness_step.py	(revision 1185)
@@ -0,0 +1,42 @@
+from typing import Dict, List
+
+from evolalg_steps.base.frams_step import FramsStep
+from evolalg_steps.base.individual import Individual
+import frams
+
+
+class FitnessStep(FramsStep):
+    def __init__(self, frams_lib, fields: Dict, fields_defaults: Dict, commands: List[str] = None,
+                 vectorized: bool = True, evaluation_count=None, *args, **kwargs):
+
+        super().__init__(frams_lib, commands, *args, **kwargs)
+        self.fields = fields
+        self.fields_defaults = fields_defaults
+        self.vectorized = vectorized
+        self.evaluation_count = evaluation_count
+        self.evaluation_count_original = None  # to be able to restore to original value after it is changed
+
+    def pre(self):
+        if self.evaluation_count is not None:
+            self.evaluation_count_original = frams.ExpProperties.evalcount._value()  # store original value and restore it in post()
+            frams.ExpProperties.evalcount = self.evaluation_count
+
+    def post(self):
+        if self.evaluation_count is not None:
+            frams.ExpProperties.evalcount = self.evaluation_count_original
+            self.evaluation_count_original = None
+
+    def call(self, population: List[Individual]):
+        super(FitnessStep, self).call(population)
+        if self.vectorized:
+            data = self.frams.evaluate([_.genotype for _ in population])
+        else:
+            data = [self.frams.evaluate([_.genotype]) for _ in population]
+
+        for ind, d in zip(population, data):
+            for k, v in self.fields.items():
+                try:
+                    setattr(ind, v, d["evaluations"][""][k])
+                except:
+                    setattr(ind, v, self.fields_defaults[k])
+        return population
Index: /framspy/evolalg_steps/fitness/multiple_evaluations.py
===================================================================
--- /framspy/evolalg_steps/fitness/multiple_evaluations.py	(revision 1185)
+++ /framspy/evolalg_steps/fitness/multiple_evaluations.py	(revision 1185)
@@ -0,0 +1,20 @@
+from typing import Dict, List
+
+from evolalg_steps.base.individual import Individual
+from evolalg_steps.fitness.fitness_step import FitnessStep
+import copy
+
+class MultipleEvaluationFitness(FitnessStep):
+    def __init__(self, frams_lib, fields: Dict, fields_defaults: Dict, commands: List[str] = None,
+                 vectorized: bool = True, number_evaluations = 10, *args, **kwargs):
+        super().__init__(frams_lib, fields, fields_defaults, commands, vectorized, *args, **kwargs)
+        self.number_evaluations = number_evaluations
+
+
+    def call(self, population: List[Individual]):
+        super(MultipleEvaluationFitness, self).call(population)
+        pop = copy.deepcopy(population)
+        statistics = [{} for _ in range(len(population))]
+        for _ in self.number_evaluations:
+            pass
+        return population
Index: /framspy/evolalg_steps/mutation_cross/frams_cross.py
===================================================================
--- /framspy/evolalg_steps/mutation_cross/frams_cross.py	(revision 1185)
+++ /framspy/evolalg_steps/mutation_cross/frams_cross.py	(revision 1185)
@@ -0,0 +1,25 @@
+import random
+
+from evolalg_steps.base.frams_step import FramsStep
+from evolalg_steps.base.individual import Individual
+
+
+class FramsCross(FramsStep):
+    def __init__(self, frams_lib, commands, cross_prob, *args, **kwargs):
+        super().__init__(frams_lib, commands, *args, **kwargs)
+        self.cross_prob = cross_prob
+
+    def call(self, population):
+        super(FramsCross, self).call(population)
+        for i in range(1, len(population), 2):
+            if random.random() < self.cross_prob:
+                geno1 = population[i - 1].genotype
+                geno2 = population[i].genotype
+
+                cross_geno1 = self.frams.crossOver(geno1, geno2)
+                cross_geno2 = self.frams.crossOver(geno1, geno2)
+
+                population[i - 1] = Individual(cross_geno1)
+                population[i] = Individual(cross_geno2)
+
+        return population
Index: /framspy/evolalg_steps/mutation_cross/frams_cross_and_mutate.py
===================================================================
--- /framspy/evolalg_steps/mutation_cross/frams_cross_and_mutate.py	(revision 1185)
+++ /framspy/evolalg_steps/mutation_cross/frams_cross_and_mutate.py	(revision 1185)
@@ -0,0 +1,20 @@
+from evolalg_steps.base.frams_step import FramsStep
+from evolalg_steps.mutation_cross.frams_cross import FramsCross
+from evolalg_steps.mutation_cross.frams_mutation import FramsMutation
+
+
+class FramsCrossAndMutate(FramsStep):
+    def __init__(self, frams_lib, cross_prob, mutate_prob, mutate_commands=None, cross_commands=None, *args, **kwargs):
+        super().__init__(frams_lib, *args, **kwargs)
+
+        self.cross_prob = cross_prob
+        self.mutate_prob = mutate_prob
+
+        self.frams_mutate = FramsMutation(frams_lib, mutate_commands, mutate_prob)
+        self.frams_cross = FramsCross(frams_lib, cross_commands, cross_prob)
+
+    def call(self, population):
+        super(FramsCrossAndMutate, self).call(population)
+        population = self.frams_cross(population)
+        population = self.frams_mutate(population)
+        return population
Index: /framspy/evolalg_steps/mutation_cross/frams_cross_or_mutate.py
===================================================================
--- /framspy/evolalg_steps/mutation_cross/frams_cross_or_mutate.py	(revision 1185)
+++ /framspy/evolalg_steps/mutation_cross/frams_cross_or_mutate.py	(revision 1185)
@@ -0,0 +1,43 @@
+from evolalg_steps.base.frams_step import FramsStep
+from evolalg_steps.mutation_cross.frams_cross import FramsCross
+from evolalg_steps.mutation_cross.frams_mutation import FramsMutation
+import random
+
+
+class FramsCrossOrMutate(FramsStep):
+    def __init__(self, frams_lib, commands, cross_prob, mutate_prob, mutate_commands=None, cross_commands=None, *args,
+                 **kwargs):
+        super().__init__(frams_lib, commands, *args, **kwargs)
+
+        self.cross_prob = cross_prob
+        self.mutate_prob = mutate_prob
+
+        self.frams_mutate = FramsMutation(frams_lib, mutate_commands, 1)
+        self.frams_cross = FramsCross(frams_lib, cross_commands, 1)
+
+    def call(self, population):
+        super(FramsCrossOrMutate, self).call(population)
+        mutate_idx = []
+        cross_idx = []
+        for i in range(len(population)):
+            rng = random.random()
+            if rng < self.cross_prob:
+                cross_idx.append(i)
+            elif rng < self.cross_prob + self.mutate_prob:
+                mutate_idx.append(i)
+
+        mutate_ind = [population[_] for _ in mutate_idx]
+        cross_ind = [population[_] for _ in cross_idx]
+
+        if len(mutate_ind) > 0:
+            self.frams_mutate(mutate_ind)
+        if len(cross_idx) > 0:
+            self.frams_cross(cross_idx)
+
+        for i, ind in zip(mutate_idx, mutate_ind):
+            population[i] = ind
+
+        for i, ind in zip(cross_idx, cross_ind):
+            population[i] = ind
+
+        return population
Index: /framspy/evolalg_steps/mutation_cross/frams_mutation.py
===================================================================
--- /framspy/evolalg_steps/mutation_cross/frams_mutation.py	(revision 1185)
+++ /framspy/evolalg_steps/mutation_cross/frams_mutation.py	(revision 1185)
@@ -0,0 +1,24 @@
+import random
+
+from evolalg_steps.base.frams_step import FramsStep
+from evolalg_steps.base.individual import Individual
+
+
+class FramsMutation(FramsStep):
+    def __init__(self, frams_lib, commands, mutate_prob, *args, **kwargs):
+        super().__init__(frams_lib, commands, *args, **kwargs)
+        self.mutate_prob = mutate_prob
+
+    def call(self, population):
+        super(FramsMutation, self).call(population)
+        idx = []
+        for i in range(len(population)):
+            if random.random() < self.mutate_prob:
+                idx.append(i)
+        mutated = [population[_].genotype for _ in idx]
+        mutated = self.frams.mutate([_ for _ in mutated])
+
+        for i, m in zip(idx, mutated):
+            population[i] = Individual(m)
+
+        return population
Index: /framspy/evolalg_steps/population/frams_population.py
===================================================================
--- /framspy/evolalg_steps/population/frams_population.py	(revision 1185)
+++ /framspy/evolalg_steps/population/frams_population.py	(revision 1185)
@@ -0,0 +1,18 @@
+from evolalg_steps.base.frams_step import FramsStep
+from evolalg_steps.base.individual import Individual
+
+
+class FramsPopulation(FramsStep):
+    def __init__(self, frams_lib, genetic_format, pop_size, commands=None, *args, **kwargs):
+
+        if commands is None:
+            commands = []
+        super().__init__(frams_lib, commands,*args, **kwargs)
+
+        self.pop_size = pop_size
+        self.genetic_format = genetic_format
+
+    def call(self, population, *args, **kwargs):
+        super(FramsPopulation, self).call(population)
+        return [Individual(self.frams.getSimplest(self.genetic_format)) for _ in range(self.pop_size)]
+
Index: /framspy/evolalg_steps/repair/__init__.py
===================================================================
--- /framspy/evolalg_steps/repair/__init__.py	(revision 1185)
+++ /framspy/evolalg_steps/repair/__init__.py	(revision 1185)
@@ -0,0 +1,1 @@
+
Index: /framspy/evolalg_steps/repair/const.py
===================================================================
--- /framspy/evolalg_steps/repair/const.py	(revision 1185)
+++ /framspy/evolalg_steps/repair/const.py	(revision 1185)
@@ -0,0 +1,12 @@
+import copy
+
+from evolalg_steps.repair.repair import Repair
+
+
+class ConstRepair(Repair):
+    def __init__(self, value, excepted_size, *args, **kwargs):
+        super(ConstRepair, self).__init__(excepted_size, *args, **kwargs)
+        self.value = value
+
+    def generate_new(self, population, missing_count):
+        return copy.deepcopy(self.value)
Index: /framspy/evolalg_steps/repair/halloffame_repair.py
===================================================================
--- /framspy/evolalg_steps/repair/halloffame_repair.py	(revision 1185)
+++ /framspy/evolalg_steps/repair/halloffame_repair.py	(revision 1185)
@@ -0,0 +1,16 @@
+import copy
+import random
+
+from evolalg_steps.repair import Repair
+
+
+class HallOfFameRepair(Repair):
+    def __init__(self, excepted_size, halloffame, top, *args, **kwargs):
+        super(HallOfFameRepair, self).__init__(excepted_size, *args, **kwargs)
+        self.halloffame = halloffame
+        self.top = top
+
+    def generate_new(self, population, missing_count):
+        ind = random.randint(0, min(self.top, len(self.halloffame)) - 1)
+        return copy.deepcopy(population[ind])
+
Index: /framspy/evolalg_steps/repair/multistep.py
===================================================================
--- /framspy/evolalg_steps/repair/multistep.py	(revision 1185)
+++ /framspy/evolalg_steps/repair/multistep.py	(revision 1185)
@@ -0,0 +1,14 @@
+from evolalg_steps.repair.repair import Repair
+import copy
+
+class MultistepRepair(Repair):
+    def __init__(self, selection, steps, excepted_size, *args, **kwargs):
+        super(MultistepRepair, self).__init__(excepted_size, *args, **kwargs)
+        self.selection = selection
+        self.steps = steps
+
+    def generate_new(self, population, missing_count):
+        selected = self.selection(population, missing_count)
+        selected = copy.deepcopy(selected)
+        selected = self.steps(selected)
+        return selected
Index: /framspy/evolalg_steps/repair/mutate.py
===================================================================
--- /framspy/evolalg_steps/repair/mutate.py	(revision 1185)
+++ /framspy/evolalg_steps/repair/mutate.py	(revision 1185)
@@ -0,0 +1,19 @@
+import random
+import copy
+
+from evolalg_steps.repair.repair import Repair
+
+
+class MutateRepair(Repair):
+    def __init__(self, mutate_step, excepted_size, iterations=1, *args, **kwargs):
+        super(MutateRepair, self).__init__(excepted_size, *args, **kwargs)
+        self.mutate_step = mutate_step
+        self.iterations = iterations
+
+
+    def generate_new(self, population, missing_count):
+        selected = population[random.randint(0, len(population))]
+        selected = copy.deepcopy(selected)
+        for _ in range(self.iterations):
+            selected = self.mutate_step([selected])[0]
+        return selected
Index: /framspy/evolalg_steps/repair/remove/field.py
===================================================================
--- /framspy/evolalg_steps/repair/remove/field.py	(revision 1185)
+++ /framspy/evolalg_steps/repair/remove/field.py	(revision 1185)
@@ -0,0 +1,12 @@
+from evolalg_steps.repair.remove.remove import Remove
+
+
+class FieldRemove(Remove):
+    def __init__(self, field_name, field_value, *args, **kwargs):
+        super(FieldRemove, self).__init__(*args, **kwargs)
+        self.field_name = field_name
+        self.field_value = field_value
+
+    def remove(self, individual):
+        return not hasattr(individual, self.field_name) or getattr(individual, self.field_name) == self.field_value
+
Index: /framspy/evolalg_steps/repair/remove/function.py
===================================================================
--- /framspy/evolalg_steps/repair/remove/function.py	(revision 1185)
+++ /framspy/evolalg_steps/repair/remove/function.py	(revision 1185)
@@ -0,0 +1,9 @@
+from evolalg_steps.repair.remove.remove import Remove
+
+class LambdaRemove(Remove):
+    def __init__(self, func, *args, **kwargs):
+        super(LambdaRemove, self).__init__(*args, **kwargs)
+        self.func = func
+
+    def remove(self, individual):
+        return self.func(individual)
Index: /framspy/evolalg_steps/repair/remove/remove.py
===================================================================
--- /framspy/evolalg_steps/repair/remove/remove.py	(revision 1185)
+++ /framspy/evolalg_steps/repair/remove/remove.py	(revision 1185)
@@ -0,0 +1,17 @@
+from abc import abstractmethod
+from evolalg_steps.base.step import Step
+
+
+
+class Remove(Step):
+    def __init__(self, *args, **kwargs):
+        super(Remove, self).__init__(*args , **kwargs)
+        pass
+
+    @abstractmethod
+    def remove(self, individual):
+        pass
+
+    def call(self, population):
+        super(Remove, self).call(population)
+        return [_ for _ in population if not self.remove(_)]
Index: /framspy/evolalg_steps/repair/repair.py
===================================================================
--- /framspy/evolalg_steps/repair/repair.py	(revision 1185)
+++ /framspy/evolalg_steps/repair/repair.py	(revision 1185)
@@ -0,0 +1,26 @@
+from abc import abstractmethod
+from collections import Iterable
+
+from evolalg_steps.base.step import Step
+
+
+class Repair(Step):
+    def __init__(self, excepted_size, *args, **kwargs):
+        super(Repair, self).__init__(*args, **kwargs)
+        self.excepted_size = excepted_size
+
+    @abstractmethod
+    def generate_new(self, population, missing_count):
+        pass
+
+    def call(self, population):
+        super(Repair, self).call(population)
+        generated = []
+        while len(generated) + len(population) < self.excepted_size:
+            gen = self.generate_new(population, self.excepted_size-len(population)-len(generated))
+            if isinstance(gen, Iterable):
+                generated.extend(gen)
+            else:
+                generated.append(gen)
+        population.extend(generated)
+        return population[:self.excepted_size]
Index: /framspy/evolalg_steps/selection/identity.py
===================================================================
--- /framspy/evolalg_steps/selection/identity.py	(revision 1185)
+++ /framspy/evolalg_steps/selection/identity.py	(revision 1185)
@@ -0,0 +1,19 @@
+import copy
+
+from evolalg_steps.base.step import Step
+from evolalg_steps.selection.selection import Selection
+
+
+class IdentitySelection(Selection):
+    def __init__(self, copy=False, *args, **kwargs):
+        super(IdentitySelection, self).__init__(copy, *args, **kwargs)
+
+    def call(self, population, selection_size=None):
+        super(IdentitySelection, self).call(population)
+        res = population
+        if selection_size is not None:
+            res = population[:selection_size]
+
+        if self.copy:
+            res = copy.deepcopy(res)
+        return res
Index: /framspy/evolalg_steps/selection/nsga2.py
===================================================================
--- /framspy/evolalg_steps/selection/nsga2.py	(revision 1185)
+++ /framspy/evolalg_steps/selection/nsga2.py	(revision 1185)
@@ -0,0 +1,17 @@
+from evolalg_steps.selection.selection import Selection
+from deap import tools
+import copy
+
+
+class NSGA2Selection(Selection):
+    def __init__(self, copy=False, *args, **kwargs):
+        super(NSGA2Selection, self).__init__(*args, **kwargs, copy=copy)
+
+    def call(self, population, count=None):
+        super(NSGA2Selection, self).call(population)
+        pop = tools.selNSGA2(population, len(population))
+        # make count divisible by 4, required by deap.tools.selTournamentDCD()
+        remainder=count % 4
+        if remainder > 0:
+            count += 4-remainder   
+        return copy.deepcopy(tools.selTournamentDCD(pop, count)) # "The individuals sequence length has to be a multiple of 4 only if k is equal to the length of individuals"
Index: /framspy/evolalg_steps/selection/selection.py
===================================================================
--- /framspy/evolalg_steps/selection/selection.py	(revision 1185)
+++ /framspy/evolalg_steps/selection/selection.py	(revision 1185)
@@ -0,0 +1,27 @@
+from abc import abstractmethod
+
+from evolalg_steps.base.step import Step
+import copy
+
+
+class Selection(Step):
+    def __init__(self, copy=False, *args, **kwargs):
+        super(Selection, self).__init__(*args, **kwargs)
+        self.copy = copy
+
+    @abstractmethod
+    def select_next(self, population):
+        pass
+
+    def call(self, population, count=None):
+        super(Selection, self).call(population)
+        res = []
+        if count is None:
+            count = len(population)
+
+        for _ in range(count):
+            sel = self.select_next(population)
+            if self.copy:
+                sel = copy.deepcopy(sel)
+            res.append(sel)
+        return res
Index: /framspy/evolalg_steps/selection/tournament.py
===================================================================
--- /framspy/evolalg_steps/selection/tournament.py	(revision 1185)
+++ /framspy/evolalg_steps/selection/tournament.py	(revision 1185)
@@ -0,0 +1,17 @@
+from evolalg_steps.base.individual import Individual
+from typing import List
+import random
+
+from evolalg_steps.base.step import Step
+from evolalg_steps.selection.selection import Selection
+
+
+class TournamentSelection(Selection):
+    def __init__(self, tournament_size: int, fit_attr="fitness", copy=False, *args, **kwargs):
+        super(TournamentSelection, self).__init__(copy, *args, **kwargs)
+        self.tournament_size = tournament_size
+        self.fit_attr = fit_attr
+
+    def select_next(self, population):
+        selected = [random.choice(population) for i in range(self.tournament_size)]
+        return max(selected, key=lambda x: getattr(x, self.fit_attr))
Index: /framspy/evolalg_steps/statistics/halloffame_custom.py
===================================================================
--- /framspy/evolalg_steps/statistics/halloffame_custom.py	(revision 1185)
+++ /framspy/evolalg_steps/statistics/halloffame_custom.py	(revision 1185)
@@ -0,0 +1,64 @@
+from deap.tools import HallOfFame
+import operator
+from copy import deepcopy
+from bisect import bisect_right
+
+class HallOfFameCustom(HallOfFame):
+    def __init__(self,  maxsize, similar=operator.eq, fitness_field="fitness", *args, **kwargs):
+        super(HallOfFameCustom, self).__init__(maxsize, similar, *args, **kwargs)
+        print("HOF_size =",self.maxsize)
+        self._fitness_field = fitness_field
+
+    def extract_fitness(self, ind):
+        return getattr(ind, self._fitness_field)
+
+    def update(self, population):
+        """Update the hall of fame with the *population* by replacing the
+        worst individuals in it by the best individuals present in
+        *population* (if they are better). The size of the hall of fame is
+        kept constant.
+        
+        :param population: A list of individual with a fitness attribute to
+                           update the hall of fame with.
+        """  
+        for ind in population:
+            if len(self) == 0 and self.maxsize != 0:
+                # Working on an empty hall of fame is problematic for the
+                # "for else"
+                for hofer in self:
+                    if self.similar(ind, hofer):
+                        break
+                else:
+                    self.insert(population[0])
+                
+                continue
+            if self.extract_fitness(ind) >= self.extract_fitness(self[-1]) or len(self) < self.maxsize:
+                for hofer in self:
+                    # Loop through the hall of fame to check for any
+                    # similar individual
+                    if self.similar(ind, hofer):
+                        break
+                else:
+                    # The individual is unique and strictly better than
+                    # the worst
+                    if len(self) >= self.maxsize:
+                        self.remove(-1)
+                    self.insert(ind)
+
+    def insert(self, item):
+        """Insert a new individual in the hall of fame using the
+        :func:`~bisect.bisect_right` function. The inserted individual is
+        inserted on the right side of an equal individual. Inserting a new
+        individual in the hall of fame also preserve the hall of fame's order.
+        This method **does not** check for the size of the hall of fame, in a
+        way that inserting a new individual in a full hall of fame will not
+        remove the worst individual to maintain a constant size.
+
+        :param item: The individual with a fitness attribute to insert in the
+                     hall of fame.
+        """
+        item = deepcopy(item)
+        i = bisect_right(self.keys, self.extract_fitness(item))
+        self.items.insert(len(self) - i, item)
+        self.keys.insert(i, self.extract_fitness(item))
+
Index: /framspy/evolalg_steps/statistics/halloffame_stats.py
===================================================================
--- /framspy/evolalg_steps/statistics/halloffame_stats.py	(revision 1185)
+++ /framspy/evolalg_steps/statistics/halloffame_stats.py	(revision 1185)
@@ -0,0 +1,14 @@
+from evolalg_steps.statistics.halloffame_custom import HallOfFameCustom
+from evolalg_steps.statistics.statistics import Statistics
+
+
+class HallOfFameStatistics(Statistics):
+    def __init__(self, size, fields="fitness", *args, **kwargs):
+        super(HallOfFameStatistics, self).__init__(*args, **kwargs)
+        self.halloffame = HallOfFameCustom(size, fitness_field=fields)
+
+    def init(self):
+        self.halloffame.clear()
+
+    def collect(self, population):
+        self.halloffame.update(population)
Index: /framspy/evolalg_steps/statistics/multistatistics_deap.py
===================================================================
--- /framspy/evolalg_steps/statistics/multistatistics_deap.py	(revision 1185)
+++ /framspy/evolalg_steps/statistics/multistatistics_deap.py	(revision 1185)
@@ -0,0 +1,26 @@
+from deap import tools
+
+from evolalg_steps.statistics.statistics import Statistics
+
+
+class MultiStatistics(Statistics):
+    def __init__(self, stats, verbose=True, *args, **kwargs):
+        super(MultiStatistics, self).__init__(*args, **kwargs)
+        self._org_stats = stats
+        self.stats = tools.MultiStatistics(**stats)
+        self.logbook = tools.Logbook()
+        self.logbook.header = ['gen'] + (self.stats.fields if self.stats else [])
+        self.verbose = verbose
+        self.gen = 0
+
+    def init(self):
+        for v in self._org_stats.values():
+            v.init()
+        self.gen = 0
+
+    def collect(self, population):
+        record = self.stats.compile(population)
+        self.logbook.record(gen=self.gen, **record)
+        self.gen += 1
+        if self.verbose:
+            print(self.logbook.stream)
Index: /framspy/evolalg_steps/statistics/statistics.py
===================================================================
--- /framspy/evolalg_steps/statistics/statistics.py	(revision 1185)
+++ /framspy/evolalg_steps/statistics/statistics.py	(revision 1185)
@@ -0,0 +1,15 @@
+from abc import abstractmethod
+
+from evolalg_steps.base.step import Step
+
+
+class Statistics(Step):
+
+    @abstractmethod
+    def collect(self, population):
+        pass
+
+    def call(self, population):
+        super(Statistics, self).call(population)
+        self.collect(population)
+        return population
Index: /framspy/evolalg_steps/statistics/statistics_deap.py
===================================================================
--- /framspy/evolalg_steps/statistics/statistics_deap.py	(revision 1185)
+++ /framspy/evolalg_steps/statistics/statistics_deap.py	(revision 1185)
@@ -0,0 +1,32 @@
+from deap import tools
+
+from evolalg_steps.statistics.statistics import Statistics
+
+
+class StatisticsDeap(Statistics):
+    def __init__(self, stats, extract_fn=lambda ind: ind.fitness, verbose=True, *args, **kwargs):
+        super(StatisticsDeap, self).__init__(*args, **kwargs)
+        self.stats = tools.Statistics(extract_fn)
+        for name, fn in stats:
+            self.stats.register(name, fn)
+
+        self.logbook = tools.Logbook()
+        self.logbook.header = ['gen'] + (self.stats.fields if self.stats else [])
+
+        self.gen = 0
+        self.verbose = verbose
+
+    def init(self):
+        self.gen = 0
+
+    def collect(self, population):
+        record = self.stats.compile(population)
+        self.logbook.record(gen=self.gen, **record)
+        self.gen += 1
+        if self.verbose:
+            print(self.logbook.stream)
+
+    def compile(self, data):
+        return self.stats.compile(data)
+
+
Index: /framspy/evolalg_steps/utils/name_propagation.py
===================================================================
--- /framspy/evolalg_steps/utils/name_propagation.py	(revision 1185)
+++ /framspy/evolalg_steps/utils/name_propagation.py	(revision 1185)
@@ -0,0 +1,15 @@
+from typing import Iterable, Union, List
+
+from evolalg_steps.base.step import Step
+import copy
+
+def propagate_names(step: Step, prefix=""):
+
+    if isinstance(step, Iterable):
+        for s in step:
+            if hasattr(step, "name"):
+                propagate_names(s, prefix=prefix + "." + step.name)
+            else:
+                propagate_names(s, prefix=prefix)
+    if hasattr(step, "name"):
+        step.name = f"{prefix}.{step.name}"
Index: /framspy/evolalg_steps/utils/population_save.py
===================================================================
--- /framspy/evolalg_steps/utils/population_save.py	(revision 1185)
+++ /framspy/evolalg_steps/utils/population_save.py	(revision 1185)
@@ -0,0 +1,38 @@
+from evolalg_steps.base.step import Step
+import os
+from framsfiles import writer as framswriter
+
+
+class PopulationSave(Step):
+    def __init__(self, path, fields, provider=None, *args, **kwargs):
+        super(PopulationSave, self).__init__(*args, **kwargs)
+        self.path = path
+        self.provider = provider
+        self.fields = fields
+
+    @staticmethod
+    def ensure_dir(path):
+        directory = os.path.dirname(path)
+        if directory == "":
+            return
+        if not os.path.exists(directory):
+            os.makedirs(directory)
+
+    def call(self, population):
+        super(PopulationSave, self).call(population)
+        PopulationSave.ensure_dir(self.path)
+        provider = self.provider
+        if provider is None:
+            provider = population
+
+        #TODO instead of "fitness", write all fields used as fitness source with their original names (e.g. "velocity", "vertpos" etc.). In evaluation, set all attributes we get from Framsticks so that here we get all original names and values. Or, even better, introduce a dict field in Individual and assign to it everything that we get from Framsticks on evaluation (and add a filtering ability, i.e. when None - save all, when a list of field names - save only the enumerated fields)
+        with open(self.path, "w") as outfile:
+            for ind in provider:
+                keyval = {}
+                for k in self.fields: # construct a dictionary with criteria names and their values
+                    keyval[k] = getattr(ind, self.fields[k])
+                outfile.write(framswriter.from_collection({"_classname": "org", **keyval}))
+                outfile.write("\n")
+
+        print("Saved '%s' (%d)" % (self.path, len(provider)))
+        return population
Index: /framspy/evolalg_steps/utils/stable_generation.py
===================================================================
--- /framspy/evolalg_steps/utils/stable_generation.py	(revision 1185)
+++ /framspy/evolalg_steps/utils/stable_generation.py	(revision 1185)
@@ -0,0 +1,37 @@
+from collections import Iterable
+
+from evolalg_steps.base.step import Step
+import copy
+
+from evolalg_steps.base.union_step import UnionStep
+
+
+class StableGeneration(Step):
+
+    def __init__(self, selection, steps, population_size=None):
+        self.selection = selection
+        self.steps = UnionStep(steps)
+        self.population_size = population_size
+
+    def generate_new(self, population, missing_count):
+        selected = self.selection(population, missing_count)
+        selected = copy.deepcopy(selected)
+        selected = self.steps(selected)
+        return selected
+
+    def init(self):
+        self.selection.init()
+        self.steps.init()
+
+    def call(self, population):
+        population_size = self.population_size
+        if population_size is None:
+            population_size = len(population)
+        generated = []
+        while len(generated) < population_size:
+            gen = self.generate_new(population, population_size - len(generated))
+            if isinstance(gen, Iterable):
+                generated.extend(gen)
+            else:
+                generated.append(gen)
+        return generated[:len(population)]
Index: amspy/run-evolalg-examples.cmd
===================================================================
--- /framspy/run-evolalg-examples.cmd	(revision 1184)
+++ 	(revision )
@@ -1,27 +1,0 @@
-rem To learn about all available options of each .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 from minimalistic example source (examples.standard)
-python -m evolalg.examples.standard          -path %DIR_WITH_FRAMS_LIBRARY%   -opt numneurons
-
-
-rem as above but "chaining" .sim files, subsequent files overwrite selected parameters
-python -m evolalg.examples.standard          -path %DIR_WITH_FRAMS_LIBRARY%   -sim eval-allcriteria.sim;deterministic.sim;sample-period-longest.sim    -opt velocity
-
-
-rem simple one-criterion evolution but more options available in examples.niching_novelty, here: hard limit on the number of Parts and debugging messages
-python -m evolalg.examples.niching_novelty   -path %DIR_WITH_FRAMS_LIBRARY%   -opt velocity   -max_numparts 6   -debug
-
-
-rem "local" niching
-python -m evolalg.examples.niching_novelty   -path %DIR_WITH_FRAMS_LIBRARY%   -opt vertpos    -fit knn_niching  -knn 3    -max_numjoints 8 -popsize 10 -generations 30
-
-
-rem two criteria, '-dissim ...' can also be used to include dissimilarity as one of the criteria
-python -m evolalg.examples.multicriteria     -path %DIR_WITH_FRAMS_LIBRARY%   -popsize 40 -generations 10 -opt velocity,vertpos 
Index: /framspy/run-evolalg_steps-examples.cmd
===================================================================
--- /framspy/run-evolalg_steps-examples.cmd	(revision 1185)
+++ /framspy/run-evolalg_steps-examples.cmd	(revision 1185)
@@ -0,0 +1,27 @@
+rem To learn about all available options of each .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 from minimalistic example source (examples.standard)
+python -m evolalg_steps.examples.standard          -path %DIR_WITH_FRAMS_LIBRARY%   -opt numneurons
+
+
+rem as above but "chaining" .sim files, subsequent files overwrite selected parameters
+python -m evolalg_steps.examples.standard          -path %DIR_WITH_FRAMS_LIBRARY%   -sim eval-allcriteria.sim;deterministic.sim;sample-period-longest.sim    -opt velocity
+
+
+rem simple one-criterion evolution but more options available in examples.niching_novelty, here: hard limit on the number of Parts and debugging messages
+python -m evolalg_steps.examples.niching_novelty   -path %DIR_WITH_FRAMS_LIBRARY%   -opt velocity   -max_numparts 6   -debug
+
+
+rem "local" niching
+python -m evolalg_steps.examples.niching_novelty   -path %DIR_WITH_FRAMS_LIBRARY%   -opt vertpos    -fit knn_niching  -knn 3    -max_numjoints 8 -popsize 10 -generations 30
+
+
+rem two criteria, '-dissim ...' can also be used to include dissimilarity as one of the criteria
+python -m evolalg_steps.examples.multicriteria     -path %DIR_WITH_FRAMS_LIBRARY%   -popsize 40 -generations 10 -opt velocity,vertpos 
