From 0b318ac0e4d969e4348289d866159dbd1526f4c1 Mon Sep 17 00:00:00 2001 From: Fabian Becker Date: Wed, 22 Jun 2022 13:15:43 +0200 Subject: [PATCH] Initial commit --- .formatter.exs | 4 +++ .gitignore | 26 ++++++++++++++ README.md | 21 +++++++++++ lib/genetic.ex | 77 +++++++++++++++++++++++++++++++++++++++++ lib/types/chromosome.ex | 12 +++++++ mix.exs | 28 +++++++++++++++ problem.ex | 9 +++++ scripts/one_max.exs | 23 ++++++++++++ test/genetic_test.exs | 8 +++++ test/test_helper.exs | 1 + 10 files changed, 209 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/genetic.ex create mode 100644 lib/types/chromosome.ex create mode 100644 mix.exs create mode 100644 problem.ex create mode 100644 scripts/one_max.exs create mode 100644 test/genetic_test.exs create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92be5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +genetic-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e116eac --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Genetic + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `genetic` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:genetic, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/lib/genetic.ex b/lib/genetic.ex new file mode 100644 index 0000000..ef07b9f --- /dev/null +++ b/lib/genetic.ex @@ -0,0 +1,77 @@ +defmodule Genetic do + alias Types.Chromosome + + def initialize(genotype, opts \\ []) do + population_size = Keyword.get(opts, :population_size, 100) + for _ <- 1..population_size, do: genotype.() + end + + def evaluate(population, fitness_function, opts \\ []) do + population + |> Enum.map( + fn chromosome -> + fitness = fitness_function.(chromosome) + age = chromosome.age + 1 + %Chromosome{chromosome | fitness: fitness, age: age} + end + ) + |> Enum.sort_by(& &1.fitness, &>=/2) + end + + def select(population, opts \\ []) do + population + |> Enum.chunk_every(2) + |> Enum.map(&List.to_tuple(&1)) + end + + def crossover(population, opts \\ []) do + population + |> Enum.reduce([], + fn {p1, p2}, acc -> + cx_point = :rand.uniform(length(p1.genes)) + {{h1,t1},{h2,t2}} = + {Enum.split(p1.genes, cx_point), + Enum.split(p2.genes, cx_point)} + {c1,c2} = {%Chromosome{p1 | genes: h1 ++ t2}, %Chromosome{p2 | genes: h2 ++ t1} } + [c1,c2 | acc] + end + ) + end + + def mutation(population, opts \\ []) do + population + |> Enum.map( + fn chromosome -> + if :rand.uniform() < 0.05 do + %Chromosome{chromosome | genes: Enum.shuffle(chromosome)} + else + chromosome + end + end + ) + end + + def run(problem, opts \\ []) do + population = initialize(&problem.genotype/0) + population + |> evolve(problem, opts) + end + + def evolve(population, problem, opts \\ []) do + population = evaluate(population, &problem.fitness_function/1, opts) + best = hd(population) + IO.write("\rCurrent Best: #{best.fitness}") + if problem.terminate?(population) do + best + else + population + |> select(opts) + |> crossover(opts) + |> mutation(opts) + |> evolve(problem, opts) + end + + + end + +end diff --git a/lib/types/chromosome.ex b/lib/types/chromosome.ex new file mode 100644 index 0000000..8014118 --- /dev/null +++ b/lib/types/chromosome.ex @@ -0,0 +1,12 @@ +defmodule Types.Chromosome do + @type t :: %__MODULE__ { + genes: Enum.t, + size: integer(), + fitness: number(), + age: integer() + } + + + @enforce_keys :genes + defstruct [:genes, size: 0, fitness: 0, age: 0] +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..d792f2f --- /dev/null +++ b/mix.exs @@ -0,0 +1,28 @@ +defmodule Genetic.MixProject do + use Mix.Project + + def project do + [ + app: :genetic, + version: "0.1.0", + elixir: "~> 1.13", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/problem.ex b/problem.ex new file mode 100644 index 0000000..0d3f56d --- /dev/null +++ b/problem.ex @@ -0,0 +1,9 @@ +defmodule Problem do + alias Types.Chromosome + + @callback genotype :: Chromosome.t + + @callback fitness_function(Chromosome.t) :: number() + + @callback terminate?(Enum.t) :: boolean() +end diff --git a/scripts/one_max.exs b/scripts/one_max.exs new file mode 100644 index 0000000..5bd36bb --- /dev/null +++ b/scripts/one_max.exs @@ -0,0 +1,23 @@ +genotype = fn -> for _ <- 1..1000, do: Enum.random(0..1) end + +fitness_function = fn chromosome -> Enum.sum(chromosome) end +max_fitness = 1000 + +defmodule OneMax do + @behaviour Problem + + @impl Problem + def genotype() do + for _ <- 1..1000, do: Enum.random(0.. + end + + @impl Problem + def fitness_function(chromosome) do + Enum.sum(chromosome.genes) + end +end + +soln = Genetic.run(fitness_function, genotype, max_fitness) + +IO.write("\n") +IO.inspect(soln) diff --git a/test/genetic_test.exs b/test/genetic_test.exs new file mode 100644 index 0000000..e91d5ab --- /dev/null +++ b/test/genetic_test.exs @@ -0,0 +1,8 @@ +defmodule GeneticTest do + use ExUnit.Case + doctest Genetic + + test "greets the world" do + assert Genetic.hello() == :world + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()