commit 6fcd8e50ff604536b78b5be27b91c3ec3f34e68c
parent be4243ee492e036b70741608e0d54c4b176e5a5e
Author: dracuxan <[email protected]>
Date: Sun, 19 Apr 2026 02:46:36 +0530
upd
Diffstat:
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)}")