# TODO Move Individual and Population to separate files.
# The Java thing is a good practice. One might even say, best practice.
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Iterator
from typing import Callable
from typing import Optional
from typing import Self
from typing import Type
from typing import Any
from typing import Union
from typing import Tuple
from typing import Dict
from _typeshed import SupportsRichComparison
from functools import wraps
from typing import overload
from typing import Iterable
from typing import Sequence
import itertools
from abc import ABC, abstractmethod, ABCMeta
from typing import Generic, TypeVar
R = TypeVar('R')
class MetaGenome(ABCMeta):
"""Machinery. Implement special behaviours in :class:`Individual`.
:meta private:
"""
def __new__(mcls: Type, name: str, bases: Tuple[type],
namespace: Dict[str, Any]) -> Any: # `Any` is BAD
ABCMeta.__init__(mcls, name, bases, namespace)
def wrap_function(custom_copy: Callable[[Individual], Individual]) -> Callable:
@wraps(custom_copy)
def wrapper(self: Individual,
*args: Any, **kwargs: Any) -> Individual:
custom_copy_result: Individual
if self.has_fitness():
old_fitness = self.fitness
custom_copy_result = custom_copy(self, *args, **kwargs)
custom_copy_result.fitness = old_fitness
else:
custom_copy_result = custom_copy(self, *args, **kwargs)
return custom_copy_result
return wrapper
namespace["copy"] = wrap_function(
namespace.setdefault("copy", lambda: None)
)
return type.__new__(mcls, name, bases, namespace)
[docs]
class Individual(ABC, Generic[R], metaclass=MetaGenome):
"""Base class for all individuals.
Derive this class to create custom representations.
Note:
An implementation should store the genotype in :attr:`.genome`.
The individual can information outside of the genotype, such as a
`.fitness`, a reference to the parent, and strategy parameter(s).
Tutorial: :doc:`../guides/examples/onemax`.
"""
[docs]
def __new__(cls: Type[Self], *args: Any, **kwargs: Any) -> Self:
"""Machinery. Implement managed attributes.
:meta private:
"""
instance: Self = super().__new__(cls)
instance._fitness = None
return instance
[docs]
@abstractmethod
def __init__(self) -> None:
#: Fitness of the individual.
self._fitness: Optional[tuple[float, ...]]
#: Genotype of the individual.
self.genome: R
@property
def fitness(self) -> tuple[float, ...]:
"""Fitness of an individual.
Writing to this property changes the fitness of the individual.
If this individual has yet to be assigned a fitness, reading
from this property raises an exception.
To determine if the individual has a fitness, call
:meth:`has_fitness`.
Return:
Fitness of the individual
Raise:
:class:`ValueError`: if the current fitness is ``None``.
"""
if (self._fitness is None):
raise ValueError("Score is accessed but null")
else:
return self._fitness
@fitness.setter
def fitness(self, value: tuple[float, ...]) -> None:
"""Sphinx does not pick up docstrings on setters.
This docstring should never be seen.
Arg:
Whatever.
"""
self._fitness = value
[docs]
def reset_fitness(self) -> None:
"""Reset the fitness of the individual.
Set the :attr:`.fitness` of the individual to ``None``.
Effect:
The `.fitness` of this individual becomes ``None``.
"""
self._fitness = None
[docs]
def has_fitness(self) -> bool:
"""Return if the individual has a fitness value.
"""
return self._fitness is not None
[docs]
@abstractmethod
def copy(self) -> Self:
"""Return an identical copy of the individual.
Subclasses should override this method.
Operations on in this individual should not affect the new individual.
In addition to duplicating :attr:`.genome`, the implementation should
decide whether to retain other fields such as :attr:`.fitness`.
Note:
Ensure that changes made to the returned value do not affect
the original value.
"""
[docs]
class AbstractCollection(ABC, Generic[R], Sequence[R], Iterable[R]):
"""Machinery.
"""
[docs]
def __init__(self, *args: R):
self._items = list(args)
self._index = 0
def __len__(self) -> int:
return len(self._items)
@overload
def __getitem__(self, key: int) -> R:
...
@overload
def __getitem__(self, key: slice) -> Sequence[R]:
...
def __getitem__(self, key: Union[int, slice]) -> R | Sequence[R]:
return self._items[key]
def __setitem__(self, key: int, value: R) -> None:
self._items[key] = value
def __delitem__(self, key: int) -> None:
del self._items[key]
def __str__(self) -> str:
return str(list(map(str, self._items)))
def __iter__(self) -> Iterator[R]:
for i in range(len(self)):
yield self[i]
def __next__(self) -> R:
if self._index < len(self._items):
old_index = self._index
self._index = self._index + 1
return self._items[old_index]
else:
raise StopIteration
[docs]
def append(self, value: R) -> None:
"""Append an item to this collection.
Args:
value: The item to add to this item
"""
# TODO value is a really bad name
self._items.append(value)
[docs]
def extend(self, values: Iterable[R]) -> None:
"""Append all items from another collection to this collection
Args:
values: Collection whose values are appended to this collection.
"""
# TODO Inefficient list comprehension. Looks awesome though.
# Improve at my own convenience.
self._items = list(itertools.chain(self._items, values))
[docs]
def populate(self, new_data: Iterable[R]) -> None:
"""Replace items in this population with items in :arg:`new_data`.
Args:
new_data: Collection whose items replace items in this
population.
Effect:
Replace all items in this population with those in :arg:`new_data`.
"""
# Redundant.
self._items = list(new_data)
[docs]
def draw(self, key: Optional[R] = None, pos: Optional[int] = None) -> R:
"""Remove an item from the population.
Identify an item either by value (in :arg:`key`) or by position
(in :arg:`pos`). Remove that item from the collection, then return that item.
Returns:
The :class:`Individual` that is removed from the population
Raises:
:class:`TypeError`: If neither :arg:`key` nor :arg:`pos` is given.
"""
if (key is None and pos is None):
raise TypeError("An item must be specified, either by"
" value or by position. Neither is given.")
elif (key is not None and pos is not None):
raise TypeError("The item can only be specified by value"
"or by position. Both are given.")
elif (pos is not None):
a: R = self[pos]
del self[pos]
return a
elif (key is not None):
has_removed = False
# TODO refactor with enumerate and filter.
# Still up for debate. Loops are easy to understand.
# Consider the trade-off.
for i in range(len(self)):
# Development mark: delete the exception when I finish this
if self[i] == key:
has_removed = True
del self[i]
break
if (not has_removed):
raise IndexError("the requested item is not in the list")
else:
return key
else:
raise RuntimeError("Values of key and pos changed during evaluation")
D = TypeVar("D", bound=Individual)
[docs]
class Population(AbstractCollection[D]):
"""A flat collection of individuals.
"""
[docs]
def __init__(self, *args: D):
"""
Args:
*args: Initial items in the population
"""
super().__init__(*args)
[docs]
def copy(self) -> Self:
"""Return an independent population.
Changes made to items in the new population should not affect
items in this population. This behaviour depends on correct implementation
of :meth:`.Individual.copy` in each item.
Call :meth:`.Individual.copy` for each :class:`.Individual` in this
population. Collect the results, then create a new population with
these values.
"""
return self.__class__(*[x.copy() for x in self._items])
[docs]
def sort(self: Self,
ranker: Callable[[D], SupportsRichComparison] = lambda x: x.fitness) -> None:
"""Rearrange items by fitness, highest-first.
If individuals have multiple fitnesses, sort lexi ... what?.
Args:
ranker: Sort key, called on each item prior to sorting.
Effect:
Rearrange items in this population.
"""
self._items.sort(reverse=True, key=ranker)
[docs]
def reset_fitness(self: Self) -> None:
"""Remove fitness values of all Individuals in the population.
Effect:
For each item in this population, set
its :attr:`.fitness Individual.fitness` to ``None``.
"""
for x in self._items:
x.reset_fitness()
[docs]
def best(self: Self) -> D:
"""Return the highest-fitness individual in this population.
"""
best_individual: D = self[0]
for x in self:
if x.fitness > best_individual.fitness:
best_individual = x
return best_individual