genetic

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

commit 5116e5a883fb41b4c07874abc52686a993f2940f
parent dd4cd4b3e88d8fe9c3076aae51086e08ca87df16
Author: dracuxan <[email protected]>
Date:   Sat, 23 May 2026 22:37:18 +0530

upd: new example (bad apple frames generator)

Diffstat:
Mmix.exs | 1+
Mmix.lock | 3+++
Ascripts/bad_apple.exs | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/basic.exs | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/n_queens.exs | 2+-
Mscripts/one_max.exs | 15+--------------
Mscripts/tetris.exs | 2+-
7 files changed, 217 insertions(+), 16 deletions(-)

diff --git a/mix.exs b/mix.exs @@ -29,6 +29,7 @@ defmodule Genetic.MixProject do {:exprof, "~> 0.2.4"}, {:alex, "~> 0.3.2"}, {:rustler, "~> 0.31.0"}, + {:vix, "~> 0.33"}, {:math, "~> 0.6.0"} ] end diff --git a/mix.lock b/mix.lock @@ -1,8 +1,10 @@ %{ "alex": {:hex, :alex, "0.3.2", "b6fa1d744783707e1ff068ecdef2eb993f9b05a2ebe9c97ba67b05902c0aaa9c", [:mix], [], "hexpm", "9d40610d06857f5a321923f28c44ca08575ad64594cbbfa4352eb115e5c0566e"}, "benchee": {:hex, :benchee, "1.5.0", "4d812c31d54b0ec0167e91278e7de3f596324a78a096fd3d0bea68bb0c513b10", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "5b075393aea81b8ae74eadd1c28b1d87e8a63696c649d8293db7c4df3eb67535"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "exprintf": {:hex, :exprintf, "0.2.1", "b7e895dfb00520cfb7fc1671303b63b37dc3897c59be7cbf1ae62f766a8a0314", [:mix], [], "hexpm", "20a0e8c880be90e56a77fcc82533c5d60c643915c7ce0cc8aa1e06ed6001da28"}, "exprof": {:hex, :exprof, "0.2.4", "13ddc0575a6d24b52e7c6809d2a46e9ad63a4dd179628698cdbb6c1f6e497c98", [:mix], [{:exprintf, "~> 0.2", [hex: :exprintf, repo: "hexpm", optional: false]}], "hexpm", "0884bcb66afc421c75d749156acbb99034cc7db6d3b116c32e36f32551106957"}, @@ -17,4 +19,5 @@ "rustler": {:hex, :rustler, "0.31.0", "7e5eefe61e6e6f8901e5aa3de60073d360c6320d9ec363027b0197297b80c46a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "99e378459bfb9c3bda6d3548b2b3bc6f9ad97f728f76bdbae7bf5c770a4f8abd"}, "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, + "vix": {:hex, :vix, "0.38.0", "77529ee4f6ced339c3d5f90a9eacf306f5b7109d3d1b5e3ef391a984ad404f75", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "dca58f654922fa678d5df8e028317483d9c0f8acb2e2714076a8468695687aa7"}, } diff --git a/scripts/bad_apple.exs b/scripts/bad_apple.exs @@ -0,0 +1,144 @@ +defmodule BadApple.FrameCtx do + use Agent + + def start_link(_opts \\ []) do + Agent.start_link(fn -> [] end, name: __MODULE__) + end + + def set_target(bits), do: Agent.update(__MODULE__, fn _ -> bits end) + def get_target(), do: Agent.get(__MODULE__, & &1) +end + +defmodule BadApple do + alias Types.Chromosome + @behaviour Problem + + @width 48 + @height 36 + @size @width * @height + + @impl true + def genotype do + genes = + Stream.repeatedly(fn -> Enum.random([0, 1]) end) + |> Enum.take(@size) + + %Chromosome{genes: genes, size: @size} + end + + @impl true + def fitness_function(%Chromosome{genes: genes}) do + target = BadApple.FrameCtx.get_target() + + matches = + Enum.zip(genes, target) + |> Enum.count(fn {a, b} -> a == b end) + + matches / @size + end + + @impl true + def terminate?(population, generation) do + best = hd(population) + best.fitness >= 0.985 or generation >= 120 + end +end + +defmodule BadApple.FrameIO do + def load_bits(path) do + {:ok, image} = Vix.Vips.Image.new_from_file(path) + width = Vix.Vips.Image.width(image) + height = Vix.Vips.Image.height(image) + {:ok, bin} = Vix.Vips.Image.write_to_binary(image) + + expected = width * height + + if byte_size(bin) != expected do + raise "Expected #{expected} bytes, got #{byte_size(bin)} for #{path}" + end + + for <<px <- bin>> do + if px >= 128, do: 1, else: 0 + end + end +end + +defmodule BadApple.Output do + @width 48 + @height 36 + + def save_bits_as_png(bits, out_path) do + bytes = + bits + |> Enum.map(fn b -> if b == 1, do: 255, else: 0 end) + |> :erlang.list_to_binary() + + {:ok, image} = + Vix.Vips.Image.new_from_binary(bytes, @width, @height, 1, :VIPS_FORMAT_UCHAR) + + :ok = File.mkdir_p!(Path.dirname(out_path)) + :ok = Vix.Vips.Image.write_to_file(image, out_path) + end +end + +{:ok, _} = BadApple.FrameCtx.start_link() +start_frame = 501 +count = 200 + +frame_paths = + "priv/frames_target/*.png" + |> Path.wildcard() + |> Enum.sort() + |> Enum.drop(start_frame - 1) + |> Enum.take(count) + +IO.puts("Processing #{length(frame_paths)} frames...") + +last_best = nil + +for {frame_path, idx} <- Enum.with_index(frame_paths, 1), reduce: last_best do + prev_best -> + target_bits = BadApple.FrameIO.load_bits(frame_path) + BadApple.FrameCtx.set_target(target_bits) + + init_type = + if prev_best do + fn genotype, opts -> + population_size = Keyword.get(opts, :population_size, 60) + seed = prev_best.genes + + seeded = + Enum.map(1..population_size, fn _ -> + genes = + Enum.map(seed, fn bit -> + if :rand.uniform() < 0.10, do: 1 - bit, else: bit + end) + + %Types.Chromosome{genes: genes, size: length(genes)} + end) + + [genotype.() | tl(seeded)] + end + else + &Toolbox.Initialize.random/2 + end + + {best, generation} = + Genetic.run(BadApple, + population_size: 100, + initialization_type: init_type, + selection_type: &Toolbox.Selection.elite/2, + mutation_type: &Toolbox.Mutation.flip/2, + mutation_rate: 0.010, + crossover_type: &Toolbox.Crossover.uniform/3 + ) + + out_path = "priv/frames_out/#{String.pad_leading(Integer.to_string(idx), 5, "0")}.png" + BadApple.Output.save_bits_as_png(best.genes, out_path) + + IO.puts( + "\nFrame #{idx} done | gen=#{generation} | fitness=#{Float.round(best.fitness, 4)} | #{Path.basename(frame_path)}" + ) + + best +end diff --git a/scripts/basic.exs b/scripts/basic.exs @@ -0,0 +1,66 @@ +defmodule Basic do + defp combination do + genes = Enum.map(0..4, fn _ -> Enum.random(0..1) end) + %{genes: genes} + end + + def fitness(c) do + gains = [6, -5, 8, 9, 7] + + frequency = + gains + |> Enum.zip(c.genes) + |> Enum.map(fn {g, c} -> g * c end) + |> Enum.sum() + + frequency + end + + def terminate?(population, _generation) do + best = hd(population) + best.fitness == 30 + end + + def population do + Enum.map(0..4, fn _ -> combination() end) + end + + def evaluate(population, fitness_func) do + population + |> Enum.map(fn c -> + fitness = fitness_func.(c) + %{genes: c.genes, fitness: fitness} + end) + |> Enum.sort_by(& &1.fitness, :desc) + end + + def selection(population) do + population + |> Enum.chunk_every(2) + end + + def crossover(population) do + population + |> Enum.reduce([], fn {c1, c2}, acc -> [c1, c2 | acc] end) + end + + def algorithm(population, generation) do + population = evaluate(population, &Basic.fitness/1) + best = hd(population) + fit_str = best.fitness + IO.write("\rcurrent best: #{fit_str}\t generation: #{generation}") + + if terminate?(population, generation) do + {best, generation} + else + population + |> selection() + |> crossover() + |> algorithm(generation + 1) + end + end +end + +population = Basic.population() +{best, _generation} = Basic.algorithm(population, 1) +IO.inspect(best) diff --git a/scripts/n_queens.exs b/scripts/n_queens.exs @@ -2,7 +2,7 @@ defmodule NQueens do alias Types.Chromosome @behaviour Problem - @max_queens 16 + @max_queens 12 @target_fitness @max_queens + 1 @size @target_fitness diff --git a/scripts/one_max.exs b/scripts/one_max.exs @@ -1,7 +1,7 @@ defmodule OneMax do alias Types.Chromosome @behaviour Problem - @max_n 1000 + @max_n 64 @impl true def genotype do @@ -18,19 +18,6 @@ defmodule OneMax do def terminate?(population, _generation) do best = Enum.max_by(population, &OneMax.fitness_function/1) best.fitness == @max_n - - # best = Enum.min_by(population, &OneMax.fitness_function/1) - # best.fitness == 0 - - # avg = - # population - # |> Enum.map(&Enum.sum(&1.genes)) - # |> Enum.sum() - # |> Kernel./(length(population)) - # - # avg >= 15 - - # generation == 100 end end diff --git a/scripts/tetris.exs b/scripts/tetris.exs @@ -44,6 +44,6 @@ defmodule Tetris do def terminate?(_population, generation), do: generation == 5 end -TetrisInterface.start_link("/home/dracuxan/elixir/src/genetic/games/tetris.bin") +TetrisInterface.start_link("/home/dracuxan/Projects/elixir/src/genetic/games/tetris.bin") soln = Genetic.run(Tetris, population_size: 10) IO.puts("\nbest: #{inspect(soln)}")