722 lines
21 KiB
Python
722 lines
21 KiB
Python
# Copyright (c) 2022, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import (
|
|
Sequence,
|
|
Iterable,
|
|
Optional,
|
|
Callable,
|
|
Union,
|
|
)
|
|
import abc
|
|
import copy
|
|
from dataclasses import dataclass
|
|
import json
|
|
import random
|
|
import time
|
|
|
|
# example usage:
|
|
# examples\addons\optimize\bin_packing_forms.py
|
|
# examples\addons\optimize\tsp.py
|
|
|
|
|
|
class DNA(abc.ABC):
|
|
"""Abstract DNA class."""
|
|
|
|
fitness: Optional[float] = None
|
|
_data: list
|
|
|
|
@abc.abstractmethod
|
|
def reset(self, values: Iterable):
|
|
...
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def is_valid(self) -> bool:
|
|
...
|
|
|
|
def copy(self):
|
|
return copy.deepcopy(self)
|
|
|
|
def _taint(self):
|
|
self.fitness = None
|
|
|
|
def __repr__(self):
|
|
return f"{self.__class__.__name__}({str(self._data)})"
|
|
|
|
def __eq__(self, other):
|
|
assert isinstance(other, self.__class__)
|
|
return self._data == other._data
|
|
|
|
def __len__(self):
|
|
return len(self._data)
|
|
|
|
def __getitem__(self, item):
|
|
return self._data.__getitem__(item)
|
|
|
|
def __setitem__(self, key, value):
|
|
self._data.__setitem__(key, value)
|
|
self._taint()
|
|
|
|
def __iter__(self):
|
|
return iter(self._data)
|
|
|
|
@abc.abstractmethod
|
|
def flip_mutate_at(self, index: int) -> None:
|
|
...
|
|
|
|
|
|
def dna_fitness(dna: DNA) -> float:
|
|
return dna.fitness # type: ignore
|
|
|
|
|
|
class Mutate(abc.ABC):
|
|
"""Abstract mutation."""
|
|
|
|
@abc.abstractmethod
|
|
def mutate(self, dna: DNA, rate: float):
|
|
...
|
|
|
|
|
|
class FlipMutate(Mutate):
|
|
"""Flip one bit mutation."""
|
|
|
|
def mutate(self, dna: DNA, rate: float):
|
|
for index in range(len(dna)):
|
|
if random.random() < rate:
|
|
dna.flip_mutate_at(index)
|
|
|
|
|
|
class NeighborSwapMutate(Mutate):
|
|
"""Swap two neighbors mutation."""
|
|
|
|
def mutate(self, dna: DNA, rate: float):
|
|
for index in range(len(dna)):
|
|
if random.random() < rate:
|
|
i2 = index - 1
|
|
tmp = dna[i2]
|
|
dna[i2] = dna[index]
|
|
dna[index] = tmp
|
|
|
|
|
|
class RandomSwapMutate(Mutate):
|
|
"""Swap two random places mutation."""
|
|
|
|
def mutate(self, dna: DNA, rate: float):
|
|
length = len(dna)
|
|
for index in range(length):
|
|
if random.random() < rate:
|
|
i2 = random.randrange(0, length)
|
|
if i2 == index:
|
|
i2 -= 1
|
|
tmp = dna[i2]
|
|
dna[i2] = dna[index]
|
|
dna[index] = tmp
|
|
|
|
|
|
class ReverseMutate(Mutate):
|
|
"""Reverse some consecutive bits mutation."""
|
|
|
|
def __init__(self, bits: int = 3):
|
|
self._bits = int(max(bits, 1))
|
|
|
|
def mutate(self, dna: DNA, rate: float):
|
|
length = len(dna)
|
|
if random.random() < rate * (
|
|
length / self._bits
|
|
): # applied to all bits at ones
|
|
i1 = random.randrange(length - self._bits)
|
|
i2 = i1 + self._bits
|
|
bits = dna[i1:i2]
|
|
dna[i1:i2] = reversed(bits)
|
|
|
|
|
|
class ScrambleMutate(Mutate):
|
|
"""Scramble some consecutive bits mutation."""
|
|
|
|
def __init__(self, bits: int = 3):
|
|
self._bits = int(max(bits, 1))
|
|
|
|
def mutate(self, dna: DNA, rate: float):
|
|
length = len(dna)
|
|
if random.random() < rate * (
|
|
length / self._bits
|
|
): # applied to all bits at ones
|
|
i1 = random.randrange(length - self._bits)
|
|
i2 = i1 + self._bits
|
|
bits = dna[i1:i2]
|
|
random.shuffle(bits)
|
|
dna[i1:i2] = bits
|
|
|
|
|
|
class Mate(abc.ABC):
|
|
"""Abstract recombination."""
|
|
|
|
@abc.abstractmethod
|
|
def recombine(self, dna1: DNA, dna2: DNA):
|
|
pass
|
|
|
|
|
|
class Mate1pCX(Mate):
|
|
"""One point crossover recombination."""
|
|
|
|
def recombine(self, dna1: DNA, dna2: DNA):
|
|
length = len(dna1)
|
|
index = random.randrange(0, length)
|
|
recombine_dna_2pcx(dna1, dna2, index, length)
|
|
|
|
|
|
class Mate2pCX(Mate):
|
|
"""Two point crossover recombination."""
|
|
|
|
def recombine(self, dna1: DNA, dna2: DNA):
|
|
length = len(dna1)
|
|
i1 = random.randrange(0, length)
|
|
i2 = random.randrange(0, length)
|
|
if i1 > i2:
|
|
i1, i2 = i2, i1
|
|
recombine_dna_2pcx(dna1, dna2, i1, i2)
|
|
|
|
|
|
class MateUniformCX(Mate):
|
|
"""Uniform recombination."""
|
|
|
|
def recombine(self, dna1: DNA, dna2: DNA):
|
|
for index in range(len(dna1)):
|
|
if random.random() > 0.5:
|
|
tmp = dna1[index]
|
|
dna1[index] = dna2[index]
|
|
dna2[index] = tmp
|
|
|
|
|
|
class MateOrderedCX(Mate):
|
|
"""Recombination class for ordered DNA like UniqueIntDNA()."""
|
|
|
|
def recombine(self, dna1: DNA, dna2: DNA):
|
|
length = len(dna1)
|
|
i1 = random.randrange(0, length)
|
|
i2 = random.randrange(0, length)
|
|
if i1 > i2:
|
|
i1, i2 = i2, i1
|
|
recombine_dna_ocx1(dna1, dna2, i1, i2)
|
|
|
|
|
|
def recombine_dna_2pcx(dna1: DNA, dna2: DNA, i1: int, i2: int) -> None:
|
|
"""Two point crossover."""
|
|
part1 = dna1[i1:i2]
|
|
part2 = dna2[i1:i2]
|
|
dna1[i1:i2] = part2
|
|
dna2[i1:i2] = part1
|
|
|
|
|
|
def recombine_dna_ocx1(dna1: DNA, dna2: DNA, i1: int, i2: int) -> None:
|
|
"""Ordered crossover."""
|
|
copy1 = dna1.copy()
|
|
replace_dna_ocx1(dna1, dna2, i1, i2)
|
|
replace_dna_ocx1(dna2, copy1, i1, i2)
|
|
|
|
|
|
def replace_dna_ocx1(dna1: DNA, dna2: DNA, i1: int, i2: int) -> None:
|
|
"""Replace a part in dna1 by dna2 and preserve order of remaining values in
|
|
dna1.
|
|
"""
|
|
old = dna1.copy()
|
|
new = dna2[i1:i2]
|
|
dna1[i1:i2] = new
|
|
index = 0
|
|
new_set = set(new)
|
|
for value in old:
|
|
if value in new_set:
|
|
continue
|
|
if index == i1:
|
|
index = i2
|
|
dna1[index] = value
|
|
index += 1
|
|
|
|
|
|
class FloatDNA(DNA):
|
|
"""Arbitrary float numbers in the range [0, 1]."""
|
|
|
|
__slots__ = ("_data", "fitness")
|
|
|
|
def __init__(self, values: Iterable[float]):
|
|
self._data: list[float] = list(values)
|
|
self._check_valid_data()
|
|
self.fitness: Optional[float] = None
|
|
|
|
@classmethod
|
|
def random(cls, length: int) -> FloatDNA:
|
|
return cls((random.random() for _ in range(length)))
|
|
|
|
@classmethod
|
|
def n_random(cls, n: int, length: int) -> list[FloatDNA]:
|
|
return [cls.random(length) for _ in range(n)]
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
return all(0.0 <= v <= 1.0 for v in self._data)
|
|
|
|
def _check_valid_data(self):
|
|
if not self.is_valid:
|
|
raise ValueError("data value out of range")
|
|
|
|
def __str__(self):
|
|
if self.fitness is None:
|
|
fitness = ", fitness=None"
|
|
else:
|
|
fitness = f", fitness={self.fitness:.4f}"
|
|
return f"{str([round(v, 4) for v in self._data])}{fitness}"
|
|
|
|
def reset(self, values: Iterable[float]):
|
|
self._data = list(values)
|
|
self._check_valid_data()
|
|
self._taint()
|
|
|
|
def flip_mutate_at(self, index: int) -> None:
|
|
self._data[index] = 1.0 - self._data[index] # flip pick location
|
|
|
|
|
|
class BitDNA(DNA):
|
|
"""One bit DNA."""
|
|
|
|
__slots__ = ("_data", "fitness")
|
|
|
|
def __init__(self, values: Iterable):
|
|
self._data: list[bool] = list(bool(v) for v in values)
|
|
self.fitness: Optional[float] = None
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
return True # everything can be evaluated to True/False
|
|
|
|
@classmethod
|
|
def random(cls, length: int) -> BitDNA:
|
|
return cls(bool(random.randint(0, 1)) for _ in range(length))
|
|
|
|
@classmethod
|
|
def n_random(cls, n: int, length: int) -> list[BitDNA]:
|
|
return [cls.random(length) for _ in range(n)]
|
|
|
|
def __str__(self):
|
|
if self.fitness is None:
|
|
fitness = ", fitness=None"
|
|
else:
|
|
fitness = f", fitness={self.fitness:.4f}"
|
|
return f"{str([int(v) for v in self._data])}{fitness}"
|
|
|
|
def reset(self, values: Iterable) -> None:
|
|
self._data = list(bool(v) for v in values)
|
|
self._taint()
|
|
|
|
def flip_mutate_at(self, index: int) -> None:
|
|
self._data[index] = not self._data[index]
|
|
|
|
|
|
class UniqueIntDNA(DNA):
|
|
"""Unique integer values in the range from 0 to length-1.
|
|
E.g. UniqueIntDNA(10) = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
|
|
Requires MateOrderedCX() as recombination class to preserve order and
|
|
validity after DNA recombination.
|
|
|
|
Requires mutation by swapping like SwapRandom(), SwapNeighbors(),
|
|
ReversMutate() or ScrambleMutate()
|
|
"""
|
|
|
|
__slots__ = ("_data", "fitness")
|
|
|
|
def __init__(self, values: Union[int, Iterable]):
|
|
self._data: list[int]
|
|
if isinstance(values, int):
|
|
self._data = list(range(values))
|
|
else:
|
|
self._data = [int(v) for v in values]
|
|
if not self.is_valid:
|
|
raise TypeError(self._data)
|
|
self.fitness: Optional[float] = None
|
|
|
|
@classmethod
|
|
def random(cls, length: int) -> UniqueIntDNA:
|
|
dna = cls(length)
|
|
random.shuffle(dna._data)
|
|
return dna
|
|
|
|
@classmethod
|
|
def n_random(cls, n: int, length: int) -> list[UniqueIntDNA]:
|
|
return [cls.random(length) for _ in range(n)]
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
return len(set(self._data)) == len(self._data)
|
|
|
|
def __str__(self):
|
|
if self.fitness is None:
|
|
fitness = ", fitness=None"
|
|
else:
|
|
fitness = f", fitness={self.fitness:.4f}"
|
|
return f"{str([int(v) for v in self._data])}{fitness}"
|
|
|
|
def reset(self, values: Iterable) -> None:
|
|
self._data = list(int(v) for v in values)
|
|
self._taint()
|
|
|
|
def flip_mutate_at(self, index: int) -> None:
|
|
raise TypeError("flip mutation not supported")
|
|
|
|
|
|
class IntegerDNA(DNA):
|
|
"""Integer values in the range from 0 to max_ - 1.
|
|
E.g. IntegerDNA([0, 1, 2, 3, 4, 0, 1, 2, 3, 4], 5)
|
|
"""
|
|
|
|
__slots__ = ("_data", "fitness")
|
|
|
|
def __init__(self, values: Iterable[int], max_: int):
|
|
self._max = int(max_)
|
|
self._data: list[int] = list(values)
|
|
if not self.is_valid:
|
|
raise TypeError(self._data)
|
|
self.fitness: Optional[float] = None
|
|
|
|
@classmethod
|
|
def random(cls, length: int, max_: int) -> IntegerDNA:
|
|
imax = int(max_)
|
|
return cls((random.randrange(0, imax) for _ in range(length)), imax)
|
|
|
|
@classmethod
|
|
def n_random(cls, n: int, length: int, max_: int) -> list[IntegerDNA]:
|
|
return [cls.random(length, max_) for _ in range(n)]
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
return all(0 <= v < self._max for v in self._data)
|
|
|
|
def __repr__(self):
|
|
return f"{self.__class__.__name__}({str(self._data)}, {self._max})"
|
|
|
|
def __str__(self):
|
|
if self.fitness is None:
|
|
fitness = ", fitness=None"
|
|
else:
|
|
fitness = f", fitness={self.fitness:.4f}"
|
|
return f"{str([int(v) for v in self._data])}{fitness}"
|
|
|
|
def reset(self, values: Iterable) -> None:
|
|
self._data = list(int(v) for v in values)
|
|
self._taint()
|
|
|
|
def flip_mutate_at(self, index: int) -> None:
|
|
self._data[index] = self._max - self._data[index] - 1
|
|
|
|
|
|
class Selection(abc.ABC):
|
|
"""Abstract selection class."""
|
|
|
|
@abc.abstractmethod
|
|
def pick(self, count: int) -> Iterable[DNA]:
|
|
...
|
|
|
|
@abc.abstractmethod
|
|
def reset(self, candidates: Iterable[DNA]):
|
|
...
|
|
|
|
|
|
class Evaluator(abc.ABC):
|
|
"""Abstract evaluation class."""
|
|
|
|
@abc.abstractmethod
|
|
def evaluate(self, dna: DNA) -> float:
|
|
...
|
|
|
|
|
|
class Log:
|
|
@dataclass
|
|
class Entry:
|
|
runtime: float
|
|
fitness: float
|
|
avg_fitness: float
|
|
|
|
def __init__(self) -> None:
|
|
self.entries: list[Log.Entry] = []
|
|
|
|
def add(self, runtime: float, fitness: float, avg_fitness: float) -> None:
|
|
self.entries.append(Log.Entry(runtime, fitness, avg_fitness))
|
|
|
|
def dump(self, filename: str) -> None:
|
|
data = [(e.runtime, e.fitness, e.avg_fitness) for e in self.entries]
|
|
with open(filename, "wt") as fp:
|
|
json.dump(data, fp, indent=4)
|
|
|
|
@classmethod
|
|
def load(cls, filename: str) -> Log:
|
|
with open(filename, "rt") as fp:
|
|
data = json.load(fp)
|
|
log = Log()
|
|
for runtime, fitness, avg_fitness in data:
|
|
log.add(runtime, fitness, avg_fitness)
|
|
return log
|
|
|
|
|
|
class HallOfFame:
|
|
def __init__(self, count):
|
|
self.count = count
|
|
self._unique_entries = dict()
|
|
|
|
def __iter__(self):
|
|
return (
|
|
self._unique_entries[k] for k in self._sorted_keys()[: self.count]
|
|
)
|
|
|
|
def _sorted_keys(self):
|
|
return sorted(self._unique_entries.keys(), reverse=True)
|
|
|
|
def add(self, dna: DNA):
|
|
assert dna.fitness is not None
|
|
self._unique_entries[dna.fitness] = dna
|
|
|
|
def get(self, count: int) -> list[DNA]:
|
|
entries = self._unique_entries
|
|
keys = self._sorted_keys()
|
|
return [entries[k] for k in keys[: min(count, self.count)]]
|
|
|
|
def purge(self):
|
|
if len(self._unique_entries) <= self.count:
|
|
return
|
|
entries = self._unique_entries
|
|
keys = self._sorted_keys()
|
|
self._unique_entries = {k: entries[k] for k in keys[: self.count]}
|
|
|
|
|
|
def threshold_filter(
|
|
candidates: Sequence[DNA], best_fitness: float, threshold: float
|
|
) -> Iterable[DNA]:
|
|
if best_fitness <= 0.0:
|
|
minimum = min(candidates, key=dna_fitness)
|
|
min_value = (1.0 - threshold) * minimum.fitness # type: ignore
|
|
else:
|
|
min_value = best_fitness * threshold
|
|
return (c for c in candidates if c.fitness > min_value) # type: ignore
|
|
|
|
|
|
class GeneticOptimizer:
|
|
"""A genetic algorithm (GA) is a meta-heuristic inspired by the process of
|
|
natural selection. Genetic algorithms are commonly used to generate
|
|
high-quality solutions to optimization and search problems by relying on
|
|
biologically inspired operators such as mutation, crossover and selection.
|
|
|
|
Source: https://en.wikipedia.org/wiki/Genetic_algorithm
|
|
|
|
This implementation searches always for the maximum fitness, fitness
|
|
comparisons are always done by the "greater than" operator (">").
|
|
The algorithm supports negative values to search for the minimum fitness
|
|
(e.g. Travelling Salesmen Problem: -900 > -1000). Reset the start fitness
|
|
by the method :meth:`reset_fitness` accordingly::
|
|
|
|
optimizer.reset_fitness(-1e99)
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
evaluator: Evaluator,
|
|
max_generations: int,
|
|
max_fitness: float = 1.0,
|
|
):
|
|
if max_generations < 1:
|
|
raise ValueError("requires max_generations > 0")
|
|
# data:
|
|
self.name = "GeneticOptimizer"
|
|
self.log = Log()
|
|
self.candidates: list[DNA] = []
|
|
|
|
# core components:
|
|
self.evaluator: Evaluator = evaluator
|
|
self.selection: Selection = RouletteSelection()
|
|
self.mate: Mate = Mate2pCX()
|
|
self.mutation = FlipMutate()
|
|
|
|
# options:
|
|
self.max_generations = int(max_generations)
|
|
self.max_fitness: float = float(max_fitness)
|
|
self.max_runtime: float = 1e99
|
|
self.max_stagnation = 100
|
|
self.crossover_rate = 0.70
|
|
self.mutation_rate = 0.01
|
|
self.elitism: int = 2
|
|
# percentage (0.1 = 10%) of candidates with least fitness to ignore in
|
|
# next generation
|
|
self.threshold: float = 0.0
|
|
|
|
# state of last (current) generation:
|
|
self.generation: int = 0
|
|
self.start_time = 0.0
|
|
self.runtime: float = 0.0
|
|
self.best_dna: DNA = BitDNA([])
|
|
self.best_fitness: float = 0.0
|
|
self.stagnation: int = 0 # generations without improvement
|
|
self.hall_of_fame = HallOfFame(10)
|
|
|
|
def reset_fitness(self, value: float) -> None:
|
|
self.best_fitness = float(value)
|
|
|
|
@property
|
|
def is_executed(self) -> bool:
|
|
return bool(self.generation)
|
|
|
|
@property
|
|
def count(self) -> int:
|
|
return len(self.candidates)
|
|
|
|
def add_candidates(self, dna: Iterable[DNA]):
|
|
if not self.is_executed:
|
|
self.candidates.extend(dna)
|
|
else:
|
|
raise TypeError("already executed")
|
|
|
|
def execute(
|
|
self,
|
|
feedback: Optional[Callable[[GeneticOptimizer], bool]] = None,
|
|
interval: float = 1.0,
|
|
) -> None:
|
|
if self.is_executed:
|
|
raise TypeError("can only run once")
|
|
if not self.candidates:
|
|
print("no DNA defined!")
|
|
t0 = time.perf_counter()
|
|
self.start_time = t0
|
|
for self.generation in range(1, self.max_generations + 1):
|
|
self.measure_fitness()
|
|
t1 = time.perf_counter()
|
|
self.runtime = t1 - self.start_time
|
|
if (
|
|
self.best_fitness >= self.max_fitness
|
|
or self.runtime >= self.max_runtime
|
|
or self.stagnation >= self.max_stagnation
|
|
):
|
|
break
|
|
if feedback and t1 - t0 > interval:
|
|
if feedback(self): # stop if feedback() returns True
|
|
break
|
|
t0 = t1
|
|
self.next_generation()
|
|
|
|
def measure_fitness(self) -> None:
|
|
self.stagnation += 1
|
|
fitness_sum: float = 0.0
|
|
for dna in self.candidates:
|
|
if dna.fitness is not None:
|
|
fitness_sum += dna.fitness
|
|
continue
|
|
fitness = self.evaluator.evaluate(dna)
|
|
dna.fitness = fitness
|
|
fitness_sum += fitness
|
|
self.hall_of_fame.add(dna)
|
|
if fitness > self.best_fitness:
|
|
self.best_fitness = fitness
|
|
self.best_dna = dna
|
|
self.stagnation = 0
|
|
|
|
self.hall_of_fame.purge()
|
|
try:
|
|
avg_fitness = fitness_sum / len(self.candidates)
|
|
except ZeroDivisionError:
|
|
avg_fitness = 0.0
|
|
self.log.add(
|
|
time.perf_counter() - self.start_time,
|
|
self.best_fitness,
|
|
avg_fitness,
|
|
)
|
|
|
|
def next_generation(self) -> None:
|
|
count = len(self.candidates)
|
|
candidates: list[DNA] = []
|
|
selector = self.selection
|
|
selector.reset(self.filter_threshold(self.candidates))
|
|
|
|
if self.elitism > 0:
|
|
candidates.extend(self.hall_of_fame.get(self.elitism))
|
|
|
|
while len(candidates) < count:
|
|
dna1, dna2 = selector.pick(2)
|
|
dna1 = dna1.copy()
|
|
dna2 = dna2.copy()
|
|
self.recombine(dna1, dna2)
|
|
self.mutate(dna1, dna2)
|
|
candidates.append(dna1)
|
|
candidates.append(dna2)
|
|
self.candidates = candidates
|
|
|
|
def filter_threshold(self, candidates: Sequence[DNA]) -> Iterable[DNA]:
|
|
if self.threshold > 0.0:
|
|
return threshold_filter(
|
|
candidates, self.best_fitness, self.threshold
|
|
)
|
|
else:
|
|
return candidates
|
|
|
|
def recombine(self, dna1: DNA, dna2: DNA):
|
|
if random.random() < self.crossover_rate:
|
|
self.mate.recombine(dna1, dna2)
|
|
|
|
def mutate(self, dna1: DNA, dna2: DNA):
|
|
self.mutation.mutate(dna1, self.mutation_rate)
|
|
self.mutation.mutate(dna2, self.mutation_rate)
|
|
|
|
|
|
def conv_negative_weights(weights: Iterable[float]) -> Iterable[float]:
|
|
# random.choices does not accept negative values: -100 -> 1/100, -10 -> 1/10
|
|
return (0.0 if w == 0.0 else 1.0 / abs(w) for w in weights)
|
|
|
|
|
|
class RouletteSelection(Selection):
|
|
"""Selection by fitness values."""
|
|
|
|
def __init__(self, negative_values: bool = False) -> None:
|
|
self._candidates: list[DNA] = []
|
|
self._weights: list[float] = []
|
|
self._negative_values = bool(negative_values)
|
|
|
|
def reset(self, candidates: Iterable[DNA]):
|
|
# dna.fitness is not None here!
|
|
self._candidates = list(candidates)
|
|
if self._negative_values:
|
|
self._weights = list(
|
|
conv_negative_weights(dna.fitness for dna in self._candidates) # type: ignore
|
|
)
|
|
else:
|
|
self._weights = [dna.fitness for dna in self._candidates] # type: ignore
|
|
|
|
def pick(self, count: int) -> Iterable[DNA]:
|
|
return random.choices(self._candidates, self._weights, k=count)
|
|
|
|
|
|
class RankBasedSelection(RouletteSelection):
|
|
"""Selection by rank of fitness."""
|
|
|
|
def reset(self, candidates: Iterable[DNA]):
|
|
# dna.fitness is not None here!
|
|
self._candidates = list(candidates)
|
|
self._candidates.sort(key=dna_fitness)
|
|
# weight of best_fitness == len(strands)
|
|
# and decreases until 1 for the least fitness
|
|
self._weights = list(range(1, len(self._candidates) + 1))
|
|
|
|
|
|
class TournamentSelection(Selection):
|
|
"""Selection by choosing the best of a certain count of candidates."""
|
|
|
|
def __init__(self, candidates: int):
|
|
self._candidates: list[DNA] = []
|
|
self.candidates = candidates
|
|
|
|
def reset(self, candidates: Iterable[DNA]):
|
|
self._candidates = list(candidates)
|
|
|
|
def pick(self, count: int) -> Iterable[DNA]:
|
|
for _ in range(count):
|
|
values = [
|
|
random.choice(self._candidates) for _ in range(self.candidates)
|
|
]
|
|
values.sort(key=dna_fitness)
|
|
yield values[-1]
|