from ..core import Selector
from ..core import Population
from ..core import Individual
import random
from typing import Self
from typing import Any
from typing import TypeVar
from typing import Sequence
from typing import Callable
from typing import override
from types import MethodType
from functools import wraps
from operator import attrgetter
D = TypeVar("D", bound=Individual[Any])
[docs]
class NullSelector(Selector[D]):
"""Selector that does nothing.
"""
[docs]
@override
def __init__(self: Self):
pass
[docs]
@override
def select_population(self: Self,
from_population: Population[D]) -> Population[D]:
"""Return all items in :arg:`from_population` in new population.
"""
return Population(from_population)
[docs]
class TruncationSelector(Selector[D]):
"""Simple selector that select individuals with highest fitness.
"""
[docs]
@override
def __init__(self: Self, budget: int):
super().__init__(budget)
[docs]
@override
def select_population(self: Self,
from_population: Population[D]) -> Population[D]:
return Population[D](sorted(list(from_population),
key=attrgetter("fitness"))[-self.budget:])
[docs]
class TournamentSelector(Selector[D]):
"""Tournament selector:
#. From the population, select uniform sample of size :arg:`bracket_size`.
#. Iterate through the sample, stop when a selection is made.
At index ``i``, select that item with probability :math:`p * (1- p)^i`.
If no selection is made when reaching the end of the sample, select
the last item.
#. Repeat until :arg:`budget` items are selected.
"""
[docs]
def __init__(self: Self, budget: int, bracket_size: int = 2,
p: float = 1):
super().__init__(budget)
self.bracket_size: int = bracket_size
self.p: float = min(2, max(p, 0))
[docs]
@override
def select(self, from_pool: Sequence[D]) -> tuple[D]:
"""Tournament selection.
Select a uniform sample, then select the best member in that sample.
"""
sample: list[D]
budget_cap: int = min(len(from_pool), self.budget)
# Ensure: the size of a sample must not exceed the output arity.
if budget_cap < self.bracket_size:
sample = list(from_pool)
else:
# For some reason `random.sample` returns a list.
sample = random.sample(tuple(from_pool), self.bracket_size)
sample.sort(key=lambda x: x.fitness, reverse=True)
# Iterate items, select each with probability p * (1 - p)**i.
for i in range(len(sample)):
if random.random() < self.p * (1 - self.p)**i:
return (sample[i],)
# If nothing is selected in the end, select the last element
return (sample[-1],)
[docs]
def Elitist(sel: Selector[D]) -> Selector[D]:
"""Decorator that adds elitism to a selector.
Retain and update the highest-fitness individual encountered so far.
Each time the selector is called, append that individual to the end
of the output population.
Modify `select_population` of `sel` to use elitism. If `sel` already
overrides `select_population`, that implementation is destroyed.
Args:
sel: A selector
Return:
A selector
"""
def wrap_function(original_select_population:
Callable[[Selector[D], Population[D]],
Population[D]])\
-> Callable[[Selector[D], Population[D]], Population[D]]:
@wraps(original_select_population)
def wrapper(self: Selector[D],
population: Population[D],
*args: Any, **kwargs: Any) -> Population[D]:
"""Context that implements elitism.
"""
population_best: D = population.best()
my_best: D
# Monkey-patch an attribute onto the selector.
# This attribute retains the HOF individual.
# Current name is taken from a randomly generated SSH pubkey.
# Nobody else will use a name *this* absurd.
BEST_INDIVIDUAL_ATTR_NAME =\
"___g1AfoA2NMh8ZZCmRJbweeee4jS1f3Y2TRPIvBmVXQP"
if not hasattr(self, BEST_INDIVIDUAL_ATTR_NAME):
setattr(self, BEST_INDIVIDUAL_ATTR_NAME,
population_best.copy())
hof_individual: D
my_best = getattr(self, BEST_INDIVIDUAL_ATTR_NAME)
if my_best.fitness > population_best.fitness:
hof_individual = my_best
else:
hof_individual = population_best
setattr(self, BEST_INDIVIDUAL_ATTR_NAME,
population_best.copy())
# Acquire results of the original selector
results: Population[D] = \
original_select_population(self, population, *args, **kwargs)
# Append the best individual to results
temp_pop = Population(results)
temp_pop.append(hof_individual.copy())
return temp_pop
return wrapper
setattr(sel, 'select_population',
MethodType(
wrap_function(sel.select_population.__func__), # type:ignore
sel))
return sel
# class SimulatedAnnealingSelector(Selector[D]):
# """Select an individual by simulated annealing.
# Simulated annealing might
# .. math::
# sss
# #. Iterate through the population, keeping track of one
# "best" individual. Mark the first individual encountered
# as best; then for each individual:
# #. If that individual has higher fitness than the current
# best, mark that individual as best.
# #. Otherwise, mark that individual as best with probability
# """
# def __init__(self: Self, budget: int, p: float = 1):
# super().__init__(budget)
# self.p: float = min(2, max(p, 0))
# @override
# def select(self, from_pool: Sequence[D], t: float) -> tuple[D]:
# """Tournament selection.
# Select a uniform sample, then select the best member in that sample.
# """
# pass