lib/genetix/evolution/crossover.ex

defmodule Genetix.Evolution.CrossOver do
  @moduledoc """
  Contain functions with different aproaches / strategies to make the crossover.
  In genetic algorithms and evolutionary computation, crossover, also called recombination,
  is a genetic operator used to combine the genetic information of two parents to generate new offspring.

  Possibles strategies:
    - Single-point
    - Order-one
    - Uniform
    - Whole arithmetic Recombination (TODO)

  [More information](https://en.wikipedia.org/wiki/Crossover_(genetic_algorithm))

  """
  alias Genetix.Types.Chromosome

  require Logger

  @spec crossover_cx_one_point(Chromosome.t(), Chromosome.t(), keyword()) ::
          {Chromosome.t(), Chromosome.t()}
  @doc """
  Single-point crossover is the most basic crossover strategy. It works like this:

    1) Choose a random number k between 0..n-1 where n is the length of the parten chromosomes. Works on blocks of genes.
    2) Split both parents at k to rpdocue four slices of genes.
    3) Swap the tails of each parent at k produce two new children.

  """
  def crossover_cx_one_point(parent_1, parent_2, opts \\ [])

  def crossover_cx_one_point(
        parent_1 = %Chromosome{genes: []},
        parent_2 = %Chromosome{genes: []},
        _opts
      ),
      do: {parent_1, parent_2}

  def crossover_cx_one_point(parent_1, parent_2, _opts) do
    cx_point = :rand.uniform(length(parent_1.genes))

    {{h1, t1}, {h2, t2}} =
      {Enum.split(parent_1.genes, cx_point), Enum.split(parent_2.genes, cx_point)}

    {%Chromosome{parent_1 | genes: h1 ++ t2}, %Chromosome{parent_2 | genes: h2 ++ t1}}
  end

  @spec order_one_crossover(Chromosome.t(), Chromosome.t(), keyword()) ::
          {Chromosome.t(), Chromosome.t()}
  @doc """
  Order-one crossover, sometimes called "Davis order" crossover, is a crossover strategy on ordered list or permutations.
  Order-one is  part of a unique set of crossover strategies that will preserve the integrity of a permutation without
  the need for chromosome repair. It works like this:

    1) Select a random slice of genes from Parent 1.
    2) Remove the values from the slice of Parent 1 from Parent 2.
    3) Insert the slice from Parten 1 into the same position in Parent 2.
    4) Repeat with a random slice from Parten 2.

  """
  def order_one_crossover(parent_1, parent_2, _opts \\ []) do
    lim = Enum.count(parent_1.genes) - 1
    # Get random range​
    {i1, i2} =
      [:rand.uniform(lim), :rand.uniform(lim)]
      |> Enum.sort()
      |> List.to_tuple()

    # parent_2 contribution​
    slice1 = Enum.slice(parent_1.genes, i1..i2)
    slice1_set = MapSet.new(slice1)
    parent_2_contrib = Enum.reject(parent_2.genes, &MapSet.member?(slice1_set, &1))
    {head1, tail1} = Enum.split(parent_2_contrib, i1)

    # parent_1 contribution​
    slice2 = Enum.slice(parent_2.genes, i1..i2)
    slice2_set = MapSet.new(slice2)
    parent_1_contrib = Enum.reject(parent_1.genes, &MapSet.member?(slice2_set, &1))
    {head2, tail2} = Enum.split(parent_1_contrib, i1)

    # Make and return​
    {c1, c2} = {head1 ++ slice1 ++ tail1, head2 ++ slice2 ++ tail2}

    {%Chromosome{
       genes: c1,
       size: parent_1.size
     },
     %Chromosome{
       genes: c2,
       size: parent_2.size
     }}
  end

  @spec uniform(Chromosome.t(), Chromosome.t(), keyword()) ::
          {Chromosome.t(), Chromosome.t()}
  @doc """
  Genes in the parent chromosome are treated separately (individually).
  Uniform crossover works by paaring corresponding genes in a chromosome an swapping them according to a rate
  defined by the `crossover_rate` hyperparameter. By default `0.5` (50% or probability to swap both genes).
  """
  def uniform(parent_1, parent_2, opts \\ []) do
    rate = Keyword.get(opts, :crossover_rate, 0.5)

    {c1, c2} =
      parent_1.genes
      |> Enum.zip(parent_2.genes)
      |> Enum.map(fn {x, y} ->
        if :rand.uniform() < rate do
          {x, y}
        else
          {y, x}
        end
      end)
      |> Enum.unzip()

    {%Chromosome{parent_1 | genes: c1}, %Chromosome{parent_2 | genes: c2}}
  end
end