from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Tuple, TypeVar, Literal
from ..core import SimpleLinearAlgorithm
from ..core import Evaluator
from ..core import Individual, Population
from ..core import Elitist, SimpleSelector
from ..core import Variator
from typing import Self, Sequence
import random
@dataclass
class ValueRange:
"""Typing machinery. Represents a range of numbers.
:meta private:
"""
lo: int
hi: int
T = TypeVar('T', bound=Individual)
[docs]
class BinaryString(Individual[int]):
"""A string of bits.
Tutorial: :doc:`../guides/examples/onemax`.
"""
[docs]
def __init__(self, value: int, size: int) -> None:
"""
Args:
value: Integer whose binary representation is used.
size: Length of the binary string
"""
self.genome: int = value
self.size: int = size
[docs]
@staticmethod
def random(size: int) -> BinaryString:
"""Return a random binary string.
Each item in the returned value may be either 1 or 0 with equal
probability.
Args:
size: Size of the generated binary string.
"""
return BinaryString(
random.getrandbits(size),
size
)
[docs]
def copy(self: Self) -> Self:
"""Return a copy of this object.
Operations performed on the returned value do not affect this object.
"""
return type(self)(self.genome, self.size)
[docs]
def get(self: Self, pos: int) -> Literal[1] | Literal[0]:
"""Return the bit at position :arg:`pos`.
Args:
pos: Position of the returned bit value.
Raise:
IndexError: If :arg:`pos` is out of range.`
"""
self._assert_pos_out_of_bound(pos)
result = (self.genome >> pos) & 1
return 1 if result == 1 else 0 # To make mypy happy
[docs]
def set(self: Self, pos: int) -> None:
"""Set the bit at position :arg:`pos` to 0.
Args:
pos: Position of the bit value to set.
Effect:
Change :attr:`.genome`.
Raise:
IndexError: If :arg:`pos` is out of range.`
"""
self._assert_pos_out_of_bound(pos)
self.genome |= 1 << pos
[docs]
def clear(self: Self, pos: int) -> None:
"""Set the bit at position :arg:`pos` to 0.
Args:
pos: Position of the bit value to clear.
Effect:
Change :attr:`.genome`.
Raise:
IndexError: If :arg:`pos` is out of range.`
"""
self._assert_pos_out_of_bound(pos)
self.genome &= ~(1 << pos)
[docs]
def flip(self: Self, pos: int) -> None:
"""Flip the bit at position :arg:`pos`.
Args:
pos: Position of the bit value to flip.
Effect:
Change :attr:`.genome`.
Raise:
IndexError: If :arg:`pos` is outside of range.
"""
self._assert_pos_out_of_bound(pos)
self.genome ^= 1 << pos
def __str__(self: Self) -> str:
size: int = self.size
return str(
(size * [0] + [int(digit) for digit in bin(self.genome)[2:]])[-size:])
def _assert_pos_out_of_bound(self: Self, pos: int) -> None:
"""Assert that an index is within bound of this bit string.
Args:
pos: An index.
Raise:
IndexError: If :arg:`pos` is outside of range :math:`[0, self.size-1]`
"""
if pos > self.size - 1:
raise IndexError(f"Index {pos} is out of bound for a binary"
f"string of length {self.size}")
[docs]
class CountBits(Evaluator[BinaryString]):
"""Count the number of ``1`` s.
Evaluator for :class:`BinaryString`. For each ``1`` in the binary string,
incur a reward of 1.
"""
[docs]
def evaluate(self, s1: BinaryString) -> tuple[float]:
return (s1.genome.bit_count(),)
[docs]
class MutateBits(Variator[BinaryString]):
"""Randomly flip each bit in the parent.
1-to-1 variator for :class:`.BinaryString`. At each bit in the parent,
flip it with probability :arg:`mutation_rate``.
"""
[docs]
def __init__(self, mutation_rate: float):
"""
Args:
mutation_rate: Probability to flip each bit in the parent.
Raise:
ValueError: If :arg:`mutation_rate` is not in range ``[0,1]``.
"""
if (mutation_rate < 0 or mutation_rate > 1):
raise ValueError(f"Mutation rate must be between 0 and 1."
f"Got: {mutation_rate}")
self.arity = 1
self.coarity = 1
self.mutation_rate = mutation_rate
[docs]
def vary(self, parents: Sequence[BinaryString]) -> Tuple[BinaryString, ...]:
offspring = parents[0].copy()
for i in range(0, offspring.size):
if (random.random() < self.mutation_rate):
offspring.flip(i)
return (offspring,)
[docs]
def trial_run() -> None:
BINSTRING_LENGTH: int = 20
POPULATION_SIZE: int = 10
GENERATION_COUNT: int = 10
init_pop = Population[BinaryString]()
for i in range(0, POPULATION_SIZE):
init_pop.append(BinaryString.random(BINSTRING_LENGTH))
evaluator = CountBits()
selector = Elitist(SimpleSelector[BinaryString](1))
variator = MutateBits(0.05)
ctrl: SimpleLinearAlgorithm = SimpleLinearAlgorithm(
population=init_pop,
variator=variator,
evaluator=evaluator,
selector=selector,
)
dicts: dict[int, Optional[tuple[float, ...]]] = {}
for i in range(GENERATION_COUNT):
ctrl.step()
dicts[i] = ctrl.population.best()
for best_individual in dicts.values():
print(best_individual.fitness)
if __name__ == "__main__":
trial_run()