Skip to content

MAP-Elites

Illuminates a 2-D behavior grid of Rastrigin with GridArchive and ea_map_elites. Fitness is on ind.fitness; the descriptor is the first two genes.

import numpy
from deap_er import Fitness, Toolbox, creator, tools

tools.rng.seed(1234)  # disables randomization

DIM = 6
LOW, UP = -5.12, 5.12
BINS = 8


def evaluate(individual):
    (score,) = tools.bm_rastrigin(individual)
    return (-score,)  # maximize; GridArchive qd_score sums wvalues


def descriptor(individual):
    return (float(individual[0]), float(individual[1]))


def setup():
    creator.create_type("FitnessMax", Fitness, weights=(1.0,))
    creator.create_type("Individual", list, fitness=creator.FitnessMax)

    toolbox = Toolbox()
    toolbox.register("attr_float", tools.rng.uniform, LOW, UP)
    toolbox.register(
        "individual",
        tools.init_repeat,
        creator.Individual,
        toolbox.attr_float,
        DIM,
    )
    toolbox.register("population", tools.init_repeat, list, toolbox.individual)
    toolbox.register("mate", tools.cx_blend_bounded, alpha=0.5, low=LOW, up=UP)
    toolbox.register(
        "mutate",
        tools.mut_polynomial_bounded,
        low=LOW,
        up=UP,
        eta=20.0,
        mut_prob=1.0 / DIM,
    )
    toolbox.register("evaluate", evaluate)

    stats = tools.Statistics(lambda ind: ind.fitness.values)
    stats.register("avg", numpy.mean)
    stats.register("max", numpy.max)
    return toolbox, stats


def print_results(archive):
    summary = archive.stats
    elites = list(archive)
    best = max(elites, key=lambda ind: ind.fitness.values) if elites else None
    print(f"\nCoverage: {summary.coverage:.3f} ({summary.num_elites}/{summary.num_cells})")
    print(f"QD score: {summary.qd_score:.3f}")
    if best is not None:
        print(f"Best elite fitness: {best.fitness.values[0]:.4f}")
        print(f"Best elite behavior: {descriptor(best)}")


def main():
    toolbox, stats = setup()
    archive = tools.GridArchive(ranges=[(LOW, UP), (LOW, UP)], bins=BINS)
    initial = toolbox.population(size=40)
    tools.ea_map_elites(
        toolbox,
        archive,
        descriptor,
        initial,
        generations=15,
        batch_size=20,
        cx_prob=0.5,
        mut_prob=0.4,
        stats=stats,
        verbose=True,
        log_time=True,
    )
    print_results(archive)


if __name__ == "__main__":
    main()

CVT archive

CvtArchive.from_samples builds Voronoi cells from k-means centroids. The same ea_map_elites loop fills those cells.

import numpy
from deap_er import Fitness, Toolbox, creator, tools

tools.rng.seed(1234)  # disables randomization

DIM = 6
LOW, UP = -5.12, 5.12
CELLS = 16


def evaluate(individual):
    (score,) = tools.bm_rastrigin(individual)
    return (-score,)


def descriptor(individual):
    return (float(individual[0]), float(individual[1]))


def setup():
    creator.create_type("FitnessMax", Fitness, weights=(1.0,))
    creator.create_type("Individual", list, fitness=creator.FitnessMax)

    toolbox = Toolbox()
    toolbox.register("attr_float", tools.rng.uniform, LOW, UP)
    toolbox.register(
        "individual",
        tools.init_repeat,
        creator.Individual,
        toolbox.attr_float,
        DIM,
    )
    toolbox.register("population", tools.init_repeat, list, toolbox.individual)
    toolbox.register("mate", tools.cx_blend_bounded, alpha=0.5, low=LOW, up=UP)
    toolbox.register(
        "mutate",
        tools.mut_polynomial_bounded,
        low=LOW,
        up=UP,
        eta=20.0,
        mut_prob=1.0 / DIM,
    )
    toolbox.register("evaluate", evaluate)
    toolbox.register("clone", tools.clone_individual)

    stats = tools.Statistics(lambda ind: ind.fitness.values)
    stats.register("max", numpy.max)
    return toolbox, stats


def print_results(archive):
    if archive.stats.num_elites == 0:
        raise RuntimeError("CVT archive stored no elites.")
    print(f"\nCVT elites: {archive.stats.num_elites}")
    print(f"QD score: {archive.stats.qd_score:.3f}")


def main():
    toolbox, stats = setup()
    samples = [[tools.rng.uniform(LOW, UP), tools.rng.uniform(LOW, UP)] for _ in range(80)]
    archive = tools.CvtArchive.from_samples(samples, k=CELLS)
    initial = toolbox.population(size=40)
    tools.ea_map_elites(
        toolbox,
        archive,
        descriptor,
        initial,
        generations=12,
        batch_size=20,
        cx_prob=0.5,
        mut_prob=0.4,
        stats=stats,
        verbose=True,
    )
    print_results(archive)


if __name__ == "__main__":
    main()

Novelty and iso+line

UnstructuredArchive keeps elites by descriptor distance. sel_novelty ranks parents by distance to the archive; mut_iso_line interpolates toward a donor elite. See roadmap item 29 and the logging tutorial.

import numpy
from deap_er import Fitness, Toolbox, creator, tools

tools.rng.seed(1234)  # disables randomization

DIM = 4
LOW, UP = 0.0, 1.0


def evaluate(individual):
    return (sum(individual),)


def descriptor(individual):
    return (float(individual[0]), float(individual[1]))


def setup(archive):
    creator.create_type("FitnessMax", Fitness, weights=(1.0,))
    creator.create_type("Individual", list, fitness=creator.FitnessMax)

    toolbox = Toolbox()
    toolbox.register("attr_float", tools.rng.uniform, LOW, UP)
    toolbox.register(
        "individual",
        tools.init_repeat,
        creator.Individual,
        toolbox.attr_float,
        DIM,
    )
    toolbox.register("population", tools.init_repeat, list, toolbox.individual)
    toolbox.register("mate", tools.cx_blend_bounded, alpha=0.5, low=LOW, up=UP)
    toolbox.register("evaluate", evaluate)
    toolbox.register("clone", tools.clone_individual)

    def mutate(individual):
        donor = archive.random_elites(1)[0] if len(archive) else individual
        return tools.mut_iso_line(individual, donor, iso=0.05, sigma=0.08, low=LOW, up=UP)

    toolbox.register("mutate", mutate)
    toolbox.register(
        "select",
        tools.sel_novelty,
        archive=archive,
        descriptor_fn=descriptor,
        k=2,
    )

    stats = tools.Statistics(lambda ind: ind.fitness.values)
    stats.register("max", numpy.max)
    return toolbox, stats


def print_results(archive):
    if archive.stats.num_elites <= 1:
        raise RuntimeError("Novelty MAP-Elites failed to illuminate more than one elite.")
    print(f"\nUnstructured elites: {archive.stats.num_elites}")
    print(f"QD score: {archive.stats.qd_score:.3f}")


def main():
    archive = tools.UnstructuredArchive(2, min_distance=0.12, max_elites=20)
    toolbox, stats = setup(archive)
    initial = toolbox.population(size=24)
    tools.ea_map_elites(
        toolbox,
        archive,
        descriptor,
        initial,
        generations=12,
        batch_size=16,
        cx_prob=0.3,
        mut_prob=0.7,
        stats=stats,
        verbose=True,
    )
    print_results(archive)


if __name__ == "__main__":
    main()