genetic

genetic algorithm framework
git clone igris.git:dracuxan/genetic.git
Log | Files | Refs | README

commit 6fcd8e50ff604536b78b5be27b91c3ec3f34e68c
parent be4243ee492e036b70741608e0d54c4b176e5a5e
Author: dracuxan <[email protected]>
Date:   Sun, 19 Apr 2026 02:46:36 +0530

upd

Diffstat:
Mgenetic/lib/application.ex | 3++-
Mgenetic/lib/genetic.ex | 31++++++++++++++++---------------
Mgenetic/lib/types/chromosome.ex | 8+++++++-
Agenetic/lib/utilities/genealogy.ex | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agenetic/scripts/tiger_sim.exs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dgenetic/scripts/tiger_simulation.exs | 83-------------------------------------------------------------------------------
6 files changed, 164 insertions(+), 100 deletions(-)

diff --git a/genetic/lib/application.ex b/genetic/lib/application.ex @@ -3,7 +3,8 @@ defmodule Genetic.Application do def start(_type, _args) do children = [ - {Utilities.Statistics, []} + {Utilities.Statistics, []}, + {Utilities.Genealogy, [Graph.new()]} ] opts = [strategy: :one_for_one, name: Genetic.Supervisor] diff --git a/genetic/lib/genetic.ex b/genetic/lib/genetic.ex @@ -2,7 +2,7 @@ defmodule Genetic do alias Types.Chromosome def run(problem, opts \\ []) do - population = initialize(&problem.genotype/0) + population = initialize(&problem.genotype/0, opts) population |> evolve(problem, 0, opts) @@ -10,7 +10,9 @@ defmodule Genetic do def initialize(genotype, opts \\ []) do population_size = Keyword.get(opts, :population_size, 100) - Enum.map(1..population_size, fn _ -> genotype.() end) + population = Enum.map(1..population_size, fn _ -> genotype.() end) + Utilities.Genealogy.add_chromosomes(population) + population end def evaluate(population, fitness_function, _opts \\ []) do @@ -49,31 +51,30 @@ defmodule Genetic do end def crossover(population, opts \\ []) do - crossover_fn = - Keyword.get( - opts, - :crossover_type, - &Toolbox.Crossover.order_one/3 - ) + crossover_fn = Keyword.get(opts, :crossover_type, &Toolbox.Crossover.order_one/3) population |> Enum.reduce( [], fn {p1, p2}, acc -> {c1, c2} = apply(crossover_fn, [p1, p2, opts]) + Utilities.Genealogy.add_chromosome(p1, p2, c1) + Utilities.Genealogy.add_chromosome(p1, p2, c2) [c1, c2 | acc] end ) end def mutation(population, opts \\ []) do - mutation_fn = Keyword.get(opts, :mutation_type, &Toolbox.Mutation.gaussian/2) + mutation_fn = Keyword.get(opts, :mutation_type, &Toolbox.Mutation.scramble/2) mutation_rate = Keyword.get(opts, :mutation_rate, 0.05) - population - |> Enum.map(fn chromosome -> + # Keep population size stable; mutate each chromosome with probability `mutation_rate`. + Enum.map(population, fn chromosome -> if :rand.uniform() < mutation_rate do - apply(mutation_fn, [chromosome, opts]) + mutant = apply(mutation_fn, [chromosome, opts]) + Utilities.Genealogy.add_chromosome(chromosome, mutant) + mutant else chromosome end @@ -84,13 +85,13 @@ defmodule Genetic do population = evaluate(population, &problem.fitness_function/1, opts) statistics(population, generation, opts) best = hd(population) - fit_str = best.fitness |> :erlang.float_to_binary(decimals: 4) - IO.write("\rcurrent best: #{fit_str}\tgeneration: #{generation}") + # fit_str = best.fitness |> :erlang.float_to_binary(decimals: 4) + IO.write("\rcurrent best: #{best.fitness}\tgeneration: #{generation}") if problem.terminate?(population, generation) do {best, generation} else - {parents, leftover} = select(population) + {parents, leftover} = select(population, opts) children = crossover(parents, opts) (children ++ leftover) diff --git a/genetic/lib/types/chromosome.ex b/genetic/lib/types/chromosome.ex @@ -7,5 +7,11 @@ defmodule Types.Chromosome do } @enforce_keys :genes - defstruct [:genes, size: 0, fitness: 0, age: 0] + defstruct [ + :genes, + id: Base.encode16(:crypto.strong_rand_bytes(64)), + size: 0, + fitness: 0, + age: 0 + ] end diff --git a/genetic/lib/utilities/genealogy.ex b/genetic/lib/utilities/genealogy.ex @@ -0,0 +1,55 @@ +defmodule Utilities.Genealogy do + use GenServer + # client + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, name: __MODULE__) + end + + def add_chromosomes(chromosomes) do + GenServer.cast(__MODULE__, {:add_chromosomes, chromosomes}) + end + + def add_chromosome(parent, child) do + GenServer.cast(__MODULE__, {:add_chromosome, parent, child}) + end + + def add_chromosome(parent_a, parent_b, child) do + GenServer.cast(__MODULE__, {:add_chromosome, parent_a, parent_b, child}) + end + + def get_tree do + GenServer.call(__MODULE__, :get_tree) + end + + # server + def init(_opts) do + {:ok, Graph.new()} + end + + def handle_cast({:add_chromosomes, chromosomes}, genealogy) do + {:noreply, Graph.add_vertices(genealogy, chromosomes)} + end + + # child is mutant of parent + def handle_cast({:add_chromosome, parent, child}, genealogy) do + new_genealogy = + genealogy + |> Graph.add_edge(parent, child) + + {:noreply, new_genealogy} + end + + # child is crossover of parents + def handle_cast({:add_chromosome, parent_a, parent_b, child}, genealogy) do + new_genealogy = + genealogy + |> Graph.add_edge(parent_a, child) + |> Graph.add_edge(parent_b, child) + + {:noreply, new_genealogy} + end + + def handle_call(:get_tree, _, genealogy) do + {:reply, genealogy, genealogy} + end +end diff --git a/genetic/scripts/tiger_sim.exs b/genetic/scripts/tiger_sim.exs @@ -0,0 +1,84 @@ +defmodule TigerSimulation do + alias Types.Chromosome + @behaviour Problem + @traits 8 + + # representing tiger genotype: + # + # | traits | 0 | 1 | + # +------------------+------------+------------+ + # | size | smaller | larger | + # | swimming ability | low | high | + # | fur color | light | dark | + # | fat stores | less | more | + # | activity period | diurnal | nocturnal | + # | hunting range | smaller | larger | + # | fur thickness | less thick | more thick | + # | tail length | smaller | larger | + + @impl true + def genotype do + genes = Enum.map(1..@traits, fn _ -> Enum.random(0..1) end) + %Chromosome{genes: genes, size: @traits} + end + + # scores for each trait: + # + # | traits | tropical | tundra | + # +------------------+----------+--------+ + # | size | 0.0 | 1.0 | + # | swimming ability | 3.0 | 3.0 | + # | fur color | 2.0 | -2.0 | + # | fat stores | -1.0 | 1.0 | + # | activity period | 0.5 | 0.5 | + # | hunting range | 1.0 | 2.0 | + # | fur thickness | -1.0 | 1.0 | + # | tail length | 0.0 | 0.0 | + + @impl true + def fitness_function(chromosome) do + # tropic_scores = [0.0, 3.0, 2.0, -1.0, 0.5, 1.0, -1.0, 0.0] + tundra_scores = [1.0, 3.0, -2.0, 1.0, 0.5, 2.0, 1.0, 0.0] + traits = chromosome.genes + + traits + |> Enum.zip(tundra_scores) + |> Enum.map(fn {t, s} -> t * s end) + |> Enum.sum() + end + + @impl true + def terminate?(_population, generation), do: generation == 1000 + + def average_tiger(population) do + genes = Enum.map(population, & &1.genes) + fitness = Enum.map(population, & &1.fitness) + ages = Enum.map(population, & &1.age) + num_tigers = length(population) + + avg_fitness = Enum.sum(fitness) / num_tigers + avg_age = Enum.sum(ages) / num_tigers + + avg_genes = + genes + |> Enum.zip() + |> Enum.map(fn t -> Enum.sum(Tuple.to_list(t)) / num_tigers end) + + %Chromosome{genes: avg_genes, age: avg_age, fitness: avg_fitness, size: length(avg_genes)} + end +end + +{tiger, _generation} = + Genetic.run(TigerSimulation, + population_size: 1000, + selection_rate: 0.9, + mutation_rate: 0.1, + mutation_type: &Toolbox.Mutation.flip/2, + crossover_type: &Toolbox.Crossover.single_point/3, + statistics: %{average_tiger: &TigerSimulation.average_tiger/1} + ) + +# IO.puts("\nbest(?) tropical tiger: #{inspect(tiger.genes)}") +IO.puts("\nbest(?) tundra tiger: #{inspect(tiger.genes)}") +# genealogy = Utilities.Genealogy.get_tree() +# IO.inspect(Graph.vertices(genealogy)) diff --git a/genetic/scripts/tiger_simulation.exs b/genetic/scripts/tiger_simulation.exs @@ -1,83 +0,0 @@ -defmodule TigerSimulation do - alias Types.Chromosome - @behaviour Problem - @traits 8 - - # representing tiger genotype: - # - # | traits | 0 | 1 | - # +------------------+------------+------------+ - # | size | smaller | larger | - # | swimming ability | low | high | - # | fur color | light | dark | - # | fat stores | less | more | - # | activity period | diurnal | nocturnal | - # | hunting range | smaller | larger | - # | fur thickness | less thick | more thick | - # | tail length | smaller | larger | - - @impl true - def genotype do - genes = Enum.map(1..@traits, fn _ -> Enum.random(0..1) end) - %Chromosome{genes: genes, size: @traits} - end - - # scores for each trait: - # - # | traits | tropical | tundra | - # +------------------+----------+--------+ - # | size | 0.0 | 1.0 | - # | swimming ability | 3.0 | 3.0 | - # | fur color | 2.0 | -2.0 | - # | fat stores | -1.0 | 1.0 | - # | activity period | 0.5 | 0.5 | - # | hunting range | 1.0 | 2.0 | - # | fur thickness | -1.0 | 1.0 | - # | tail length | 0.0 | 0.0 | - - @impl true - def fitness_function(chromosome) do - tropic_scores = [0.0, 3.0, 2.0, -1.0, 0.5, 1.0, -1.0, 0.0] - # tundra_scores = [1.0, 3.0, -2.0, 1.0, 0.5, 2.0, 1.0, 0.0] - traits = chromosome.genes - - traits - |> Enum.zip(tropic_scores) - |> Enum.map(fn {t, s} -> t * s end) - |> Enum.sum() - end - - @impl true - def terminate?(_population, generation), do: generation == 50 - - def average_tiger(population) do - genes = Enum.map(population, & &1.genes) - fitness = Enum.map(population, & &1.fitness) - ages = Enum.map(population, & &1.age) - num_tigers = length(population) - - avg_fitness = Enum.sum(fitness) / num_tigers - avg_age = Enum.sum(ages) / num_tigers - - avg_genes = - genes - |> Enum.zip() - |> Enum.map(fn t -> Enum.sum(Tuple.to_list(t)) / num_tigers end) - - %Chromosome{genes: avg_genes, age: avg_age, fitness: avg_fitness, size: length(avg_genes)} - end -end - -{_tiger, generation} = - Genetic.run(TigerSimulation, - population_size: 100, - selection_rate: 0.9, - mutation_rate: 0.1, - mutation_type: &Toolbox.Mutation.flip/2, - crossover_type: &Toolbox.Crossover.single_point/3, - statistics: %{average_tiger: &TigerSimulation.average_tiger/1} - ) - -{_, lts} = Utilities.Statistics.lookup(generation) - -IO.puts("\n #{inspect(lts)}")