Skip to main content

README.md

# Petri

A multi-representation genetic algorithm library for Elixir.

Supports three chromosome encodings:
real (continuous), permutation (ordering), and binary. Each has its own
crossover and mutation operators. Selection, termination, and the
generational engine are shared across all three.

## Usage

Three snippets from `examples/`.

### Permutation: TSP on Berlin52

```elixir
alias Petri.Chromosome.Permutation

fitness = fn %Permutation{genes: tour} ->
  1.0 / tour_distance(tour)
end

result =
  Petri.run(fitness, %{
    encoding: :permutation,
    n: 52,
    population_size: 400,
    max_generations: 1000,
    seed: 67,
    selection: :tournament,
    tournament_size: 5,
    elite_count: 8,
    crossover: :ox,
    mutation: :inversion
  })

{best_tour, best_fitness} = result.best
best_distance = 1.0 / best_fitness
```

From [`examples/tsp.exs`](examples/tsp.exs).

### Real: ML hyperparameter tuning

```elixir
alias Petri.Chromosome.Real

fitness = fn %Real{genes: [lr, lambda, epochs]} ->
  epochs_int = max(1, round(epochs))
  weights = train(train_x, train_y, lr, lambda, epochs_int)
  r2(test_y, Nx.dot(test_x, weights))
end

result =
  Petri.run(fitness, %{
    encoding: :real,
    bounds: [{1.0e-4, 1.0e-1}, {1.0e-6, 1.0e-1}, {10.0, 200.0}],
    population_size: 50,
    max_generations: 40,
    seed: 42,
    selection: :tournament,
    tournament_size: 3,
    elite_count: 3,
    crossover: :blx_alpha,
    blx_alpha_param: 0.5,
    mutation: :gaussian,
    gaussian_sigma: 0.15,
    mutation_rate: 0.3
  })

{best_chromosome, best_fitness} = result.best
[lr, lambda, epochs] = best_chromosome.genes
```

From [`examples/ml_hyperparams.exs`](examples/ml_hyperparams.exs).

### Binary: feature subset selection

```elixir
alias Petri.Chromosome.Binary

fitness = fn %Binary{genes: mask} ->
  predictions =
    Enum.map(data, fn row ->
      row |> Enum.zip(mask)
      |> Enum.reduce(0.0, fn {v, m}, acc -> if m == 1, do: acc + v, else: acc end)
    end)

  mse =
    Enum.zip(predictions, targets)
    |> Enum.reduce(0.0, fn {p, t}, acc -> acc + (p - t) ** 2 end)
    |> Kernel./(@n_samples)

  1.0 / (1.0 + mse)
end

result =
  Petri.run(fitness, %{
    encoding: :binary,
    length: 20,
    population_size: 80,
    max_generations: 80,
    seed: 17,
    selection: :tournament,
    tournament_size: 3,
    elite_count: 3,
    crossover: :uniform,
    mutation: :bit_flip,
    mutation_rate: 0.4,
    mutation_per_gene_rate: 0.05
  })

{best_chromosome, best_fitness} = result.best
```

From [`examples/feature_selection.exs`](examples/feature_selection.exs).

## Running the examples

Standalone `.exs` scripts that use `Mix.install` to pull in dependencies.
Run from the repo root with `elixir` (not `mix`):

```
elixir examples/tsp.exs
elixir examples/ml_hyperparams.exs
elixir examples/feature_selection.exs
```

| Example | Encoding | What it does |
|---|---|---|
| `tsp.exs` | permutation | Order crossover + swap mutation on Berlin52 |
| `ml_hyperparams.exs` | real | BLX-α + Gaussian mutation tuning a linear regression |
| `feature_selection.exs` | binary | Uniform crossover + bit-flip for feature subset selection |

## Installation

```elixir
def deps do
  [
    {:petri, "~> 0.1.0"}
  ]
end
```