Skip to main content

lib/examples/hello_world.ex

defmodule Jido.Evolve.Examples.HelloWorld do
  @moduledoc """
  A simple example of evolving text towards a target string.

  This example demonstrates how to use Jido.Evolve to evolve a string
  towards the target "Hello, world!" using random mutations and
  tournament selection with adaptive mutation rates.
  """

  use Jido.Evolve.Fitness

  @target "Hello, world!"

  @doc """
  Fitness function that measures similarity to the target string.

  Higher scores indicate better fitness (closer to target).
  """
  def evaluate(text, _context) do
    # Count matching characters at correct positions
    target_chars = String.graphemes(@target)
    text_chars = String.graphemes(text)

    matches =
      Enum.zip(target_chars, text_chars)
      |> Enum.count(fn {t, c} -> t == c end)

    # Normalize to 0.0-1.0
    similarity = matches / String.length(@target)
    {:ok, similarity}
  end

  def batch_evaluate(entities, context) do
    results =
      Enum.map(entities, fn entity ->
        {:ok, score} = evaluate(entity, context)
        {entity, score}
      end)

    {:ok, results}
  end

  @doc """
  Run the hello world evolution example.

  ## Options

  - `:population_size` - Size of the population (default: 100)
  - `:generations` - Maximum generations (default: 300) 
  - `:mutation_rate` - Mutation rate (default: 0.3)
  - `:crossover_rate` - Crossover rate (default: 0.8)
  - `:elitism_rate` - Elitism rate (default: 0.02)
  - `:target_fitness` - Stop when fitness reaches this value (default: 0.99)
  - `:seed` - Initial population (default: 100 random strings)
  - `:verbose` - Print progress (default: false)

  ## Examples

      # Run with defaults
      Jido.Evolve.Examples.HelloWorld.run()
      
      # Run with custom settings
      Jido.Evolve.Examples.HelloWorld.run(
        population_size: 50,
        mutation_rate: 0.6,
        verbose: true
      )
  """
  def run(opts \\ []) do
    population_size = Keyword.get(opts, :population_size, 100)
    generations = Keyword.get(opts, :generations, 300)
    mutation_rate = Keyword.get(opts, :mutation_rate, 0.3)
    crossover_rate = Keyword.get(opts, :crossover_rate, 0.8)
    elitism_rate = Keyword.get(opts, :elitism_rate, 0.02)
    target_fitness = Keyword.get(opts, :target_fitness, 0.99)
    seed = Keyword.get(opts, :seed, Enum.map(1..population_size, fn _ -> random_string() end))
    verbose = Keyword.get(opts, :verbose, false)

    # Create configuration with adaptive mutation
    {:ok, config} =
      Jido.Evolve.Config.new(
        population_size: population_size,
        generations: generations,
        mutation_rate: mutation_rate,
        crossover_rate: crossover_rate,
        elitism_rate: elitism_rate,
        selection_strategy: Jido.Evolve.Selection.Tournament,
        mutation_strategy: Jido.Evolve.Mutation.AdaptiveText,
        crossover_strategy: Jido.Evolve.Crossover.String,
        termination_criteria: [target_fitness: target_fitness]
      )

    if verbose do
      IO.puts("Starting evolution:")
      IO.puts("Target: #{@target}")
      IO.puts("Initial: #{Enum.join(seed, ", ")}")
      IO.puts("Population size: #{population_size}")
      IO.puts("Mutation rate: #{mutation_rate} (adaptive: 0.3 → 0.08 at fitness > 0.75)")
      IO.puts("")
    end

    # Run evolution
    result =
      Jido.Evolve.evolve(
        initial_population: seed,
        config: config,
        fitness: __MODULE__
      )
      |> Stream.with_index()
      |> Stream.map(fn {state, generation} ->
        if verbose and rem(generation, 10) == 0 do
          IO.puts("Generation #{generation}: #{state.best_entity} (fitness: #{Float.round(state.best_score, 4)})")
        end

        state
      end)
      |> Stream.take_while(fn state ->
        state.best_score < target_fitness and state.generation < generations
      end)
      |> Enum.to_list()
      |> List.last()

    if result do
      if verbose do
        IO.puts("")
        IO.puts("Evolution completed!")
        IO.puts("Final result: #{result.best_entity}")
        IO.puts("Final fitness: #{Float.round(result.best_score, 6)}")
        IO.puts("Generations: #{result.generation}")
      end

      %{
        best_entity: result.best_entity,
        best_score: result.best_score,
        generation: result.generation,
        target: @target,
        success: result.best_score >= target_fitness
      }
    else
      if verbose do
        IO.puts("Evolution failed to converge")
      end

      %{
        best_entity: nil,
        best_score: 0.0,
        generation: 0,
        target: @target,
        success: false
      }
    end
  end

  @doc """
  Run a quick demo that prints progress.
  """
  def demo do
    IO.puts("Jido.Evolve Hello World Evolution Demo")
    IO.puts("=" |> String.duplicate(40))

    run(verbose: true)
  end

  defp random_string do
    chars = String.graphemes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ,!?")

    Enum.map_join(1..String.length(@target), "", fn _ -> Enum.random(chars) end)
  end
end