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:
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)}")