"""Shared representations serving as base classes for all systems."""
from copy import deepcopy as _dc
from ... import exceptions as _exceptions
from ..._utilities import argument_processing as _ap
from ..._utilities.operating_system import NEWLINE as _NEWLINE
[docs]class BaseGenotype:
"""Base genotype for all systems to define a shared structure."""
__slots__ = ("data", "_hash")
# Immutable data attribute: provided once at object creation and converted system-dependently
def __setattr__(self, key, val):
"""Implement attribute assignment to ensure data is immutable.
References
----------
- https://docs.python.org/3/reference/datamodel.html#object.__setattr__
"""
if key == "data":
_exceptions.raise_data_write_error()
object.__setattr__(self, key, val)
# Copying
[docs] def copy(self):
"""Create a deep copy of the genotype."""
return self.__class__(self.data)
def __copy__(self):
"""Create a deep copy of the genotype."""
return self.__class__(self.data)
def __deepcopy__(self, memo):
"""Create a deep copy of the genotype."""
return self.__class__(self.data)
# Representation
def __repr__(self):
"""Compute the "official" string representation of the genotype."""
return "<{} genotype at {}>".format(self._label, hex(id(self)))
def __str__(self):
"""Compute the "informal" string representation of the genotype."""
return str(self.data).replace(" ", "")
# Length
def __len__(self):
"""Calculate the length of the genotype."""
return len(self.data)
# Equality
def __eq__(self, other):
"""Compute whether two genotypes are equal."""
if isinstance(other, self.__class__):
return self.data == other.data
return NotImplemented
def __ne__(self, other):
"""Compute whether two genotypes are not equal."""
if isinstance(other, self.__class__):
return self.data != other.data
return NotImplemented
# Hashing
def __hash__(self):
"""Calculate a hash value for this object.
It is used for operations on hashed collections such as `set`
and `dict`.
References
----------
- https://docs.python.org/3/reference/datamodel.html#object.__hash__
"""
try:
return self._hash
except AttributeError:
self._hash = hash(self.data)
return self._hash
[docs]class BaseIndividual:
"""Base individual for all systems to define a shared structure."""
__slots__ = ("genotype", "phenotype", "fitness", "details")
[docs] def __init__(
self, genotype=None, phenotype=None, fitness=float("nan"), details=None
):
"""Create an individual as simple container for genotype, phenotype and fitness."""
# Argument processing
if details is None:
details = {}
# Assignments
self.genotype = genotype
self.phenotype = phenotype
self.fitness = fitness
self.details = details
# Copying: Phenotype & fitness are immutable objects (None, str, int, float) requiring no copy
[docs] def copy(self):
"""Create a deep copy of the individual."""
return self.__class__(
_dc(self.genotype), self.phenotype, self.fitness, _dc(self.details)
)
def __copy__(self):
"""Create a deep copy of the individual."""
return self.__class__(
_dc(self.genotype), self.phenotype, self.fitness, _dc(self.details)
)
def __deepcopy__(self, memo):
"""Create a deep copy of the individual."""
return self.__class__(
_dc(self.genotype), self.phenotype, self.fitness, _dc(self.details)
)
# Representation
def __repr__(self):
"""Compute the "official" string representation of the individual."""
return "<{} individual object at {}>".format(self._label, hex(id(self)))
def __str__(self):
"""Compute the "informal" string representation of the individual."""
text = (
"{lab} individual:{nl}"
"- Genotype: {gt}{nl}"
"- Phenotype: {phe}{nl}"
"- Fitness: {fit}".format(
lab=self._label,
nl=_NEWLINE,
gt=self.genotype,
phe=self.phenotype,
fit=self.fitness,
)
)
return text
# Fitness comparison: objective-dependent NaN treatment, therefore not using __lt__ and __gt__
[docs] def less_than(self, other, objective):
"""Determine if the fitness of this individual is less than that of another.
Parameters
----------
other : `Individual`
objective : `str`
Possible values:
- ``"min"`` for a minimization problem
- ``"max"`` for a maximization problem
It determines how ``NaN`` values are treated in the
comparsion.
Notes
-----
There is a conceptual problem with ``NaN`` values, making the
comparison depending on the type of optimization problem being
tackled. In case of a minimization problem, any valid `float`
number should be considered to be smaller than ``NaN``, so that
the individuals with ``NaN`` fitnesses loose in comparisons.
In case of a maximization problem, it is the other way around.
Therefore this explicit method with the argument ``objective``
is provided instead of the special method ``__lt__`` that would
allow individuals to be compared with the ``<`` operator but
without any arguments.
References
----------
- https://docs.python.org/3/library/operator.html
"""
# Argument processing: no checks for objective in ('min', 'max') to improve speed
f1 = self.fitness
f2 = other.fitness
# Regular case: 0 NaN values
if f1 == f1 and f2 == f2:
return f1 < f2
# Special cases: 1 or 2 NaN values
if f1 != f1:
if f2 != f2:
# NaN < NaN: False
return False
# NaN < number: True if maximization, False if minimization
return objective == "max"
# number < NaN: True if minimization, False if maximization
return objective == "min"
[docs] def greater_than(self, other, objective):
"""Determine if the fitness of this individual is greater than that of another.
Parameters
----------
other : `Individual`
objective : `str`
Possible values:
- ``"min"`` for a minimization problem
- ``"max"`` for a maximization problem
It determines how ``NaN`` values are treated in the
comparsion.
Notes
-----
There is a conceptual problem with ``NaN`` values, making the
comparison depending on the type of optimization problem being
tackled. In case of a minimization problem, any valid `float`
number should be considered to be smaller than ``NaN``, so that
the individuals with ``NaN`` fitnesses loose in comparisons.
In case of a maximization problem, it is the other way around.
Therefore this explicit method with the argument ``objective``
is provided instead of the special method ``__gt__`` that would
allow individuals to be compared with the ``>`` operator but
without any arguments.
References
----------
- https://docs.python.org/3/library/operator.html
"""
# Argument processing: no checks for objective in ('min', 'max') to improve speed
f1 = self.fitness
f2 = other.fitness
# Regular case: 0 NaN values
if f1 == f1 and f2 == f2:
return f1 > f2
# Special cases: 1 or 2 NaN values
if f1 != f1:
if f2 != f2:
# NaN > NaN: False
return False
# NaN > number: True if minimization, False if maximization
return objective == "min"
# number > NaN: True if maximization, False if minimization
return objective == "max"
[docs]class BasePopulation:
"""Base population for all systems to define a shared structure."""
__slots__ = "individuals"
[docs] def __init__(self, individuals):
"""Create a population as container for multiple individuals."""
# Argument processing
_ap.check_arg("individuals", individuals, types=(list, tuple))
# Assignments
self.individuals = individuals
# Copying
[docs] def copy(self):
"""Create a deep copy of the population."""
return self.__class__([ind.copy() for ind in self.individuals])
def __copy__(self):
"""Create a deep copy of the population."""
return self.__class__([ind.copy() for ind in self.individuals])
def __deepcopy__(self, memo):
"""Create a deep copy of the population."""
return self.__class__([ind.copy() for ind in self.individuals])
# Representation
def __repr__(self):
"""Compute the "official" string representation of the population."""
return "<{} population at {}>".format(self._label, hex(id(self)))
def __str__(self):
"""Compute the "informal" string representation of the population."""
text = (
"{lab} population:{nl}"
"- Individuals: {ind}{nl}"
"- Unique genotypes: {gt}{nl}"
"- Unique phenotypes: {phe}{nl}"
"- Unique fitnesses: {fit}".format(
lab=self._label,
nl=_NEWLINE,
ind=len(self),
gt=self.num_unique_genotypes,
phe=self.num_unique_phenotypes,
fit=self.num_unique_fitnesses,
)
)
return text
# Access
def __getitem__(self, key):
"""Implement index-based access for the population's individuals.
References
----------
- https://docs.python.org/3/reference/datamodel.html#object.__getitem__
"""
return self.individuals[key]
def __setitem__(self, key, value):
"""Implement index-based modification for the population's individuals.
References
----------
- https://docs.python.org/3/reference/datamodel.html#object.__setitem__
"""
if not isinstance(value, BaseIndividual):
_exceptions.raise_pop_assignment_error(value)
self.individuals[key] = value
def __delitem__(self, key):
"""Implement index-based deletion for the population's individuals.
References
----------
- https://docs.python.org/3/reference/datamodel.html#object.__delitem__
"""
del self.individuals[key]
# Length
def __len__(self):
"""Calculate the number of individuals in a population."""
return len(self.individuals)
# Iteration
def __iter__(self):
"""Implement the return of an iterator object.
References
----------
- https://docs.python.org/3/reference/datamodel.html#object.__iter__
- https://docs.python.org/3/library/stdtypes.html#container.__iter__
"""
return iter(self.individuals)
# Concatenation
def __add__(self, other):
"""Implement concatenation of populations by the plus symbol."""
return self.__class__(self.individuals + other.individuals)
@property
def num_unique_genotypes(self):
"""Get the number of unique genotypes in this population."""
return len({str(ind.genotype) for ind in self.individuals})
@property
def num_unique_phenotypes(self):
"""Get the number of unique phenotypes in this population."""
return len({str(ind.phenotype) for ind in self.individuals})
@property
def num_unique_fitnesses(self):
"""Get the number of unique fitness values in this population."""
return len({str(ind.fitness) for ind in self.individuals})