Back to Blog
Evolution March 17, 2026 | 9 min read

MENDEL: Genetic Algorithm System for Platform Evolution

How the MENDEL system uses genetic operators -- crossover, mutation, selection -- to evolve platform configurations, fitness evaluation, and generation tracking in Elixir.

Tomas Korcak (korczis)

Prismatic Platform

The MENDEL system applies genetic algorithm principles to platform evolution. Rather than manually tuning configurations, pipeline parameters, and quality thresholds, MENDEL breeds populations of candidate configurations, evaluates their fitness against real platform metrics, and selects the fittest for the next generation. This post covers the core genetic operators, fitness evaluation, and generation management implemented in Elixir.


Core Concepts


MENDEL operates on a population of "chromosomes" -- each chromosome is a set of platform configuration parameters encoded as a map. The system evolves these through standard genetic operators:


OperatorPurposeImplementation

|----------|---------|----------------|

SelectionChoose parents for reproductionTournament selection (k=3) CrossoverCombine traits from two parentsUniform crossover with 50% swap rate MutationIntroduce random variationGaussian perturbation (sigma=0.1) ElitismPreserve best individualsTop 10% pass unchanged FitnessEvaluate configuration qualityMulti-objective weighted sum

Chromosome Representation


Each chromosome encodes configuration parameters as a map of genes. Genes have types (float, integer, enum) and valid ranges:



defmodule PrismaticMendel.Chromosome do

@moduledoc """

Chromosome representation for genetic algorithm.


A chromosome is a collection of genes, each with a name,

type, value, and valid range. Provides encoding/decoding

between configuration maps and genetic representations.

"""


@type gene :: %{

name: atom(),

type: :float | :integer | :enum,

value: term(),

min: number() | nil,

max: number() | nil,

options: [atom()] | nil

}


@type t :: %__MODULE__{

id: String.t(),

genes: [gene()],

fitness: float() | nil,

generation: non_neg_integer(),

metadata: map()

}


defstruct [:id, :genes, :fitness, :generation, metadata: %{}]


@spec new(keyword()) :: t()

def new(opts) do

%__MODULE__{

id: generate_id(),

genes: Keyword.fetch!(opts, :genes),

fitness: nil,

generation: Keyword.get(opts, :generation, 0),

metadata: Keyword.get(opts, :metadata, %{})

}

end


@spec to_config(t()) :: map()

def to_config(%__MODULE__{genes: genes}) do

Map.new(genes, fn gene -> {gene.name, gene.value} end)

end


@spec from_config(map(), [gene()]) :: t()

def from_config(config, gene_templates) do

genes =

Enum.map(gene_templates, fn template ->

%{template | value: Map.get(config, template.name, template.value)}

end)


new(genes: genes)

end


defp generate_id, do: Base.encode16(:crypto.strong_rand_bytes(8), case: :lower)

end


Genetic Operators


The operators module implements selection, crossover, and mutation with configurable parameters:



defmodule PrismaticMendel.Operators do

@moduledoc """

Genetic operators for population evolution.


Implements tournament selection, uniform crossover,

Gaussian mutation, and elitism preservation. All operators

are pure functions operating on chromosome structs.

"""


alias PrismaticMendel.Chromosome


@spec tournament_select([Chromosome.t()], non_neg_integer()) :: Chromosome.t()

def tournament_select(population, tournament_size \\ 3) do

population

|> Enum.take_random(tournament_size)

|> Enum.max_by(& &1.fitness)

end


@spec crossover(Chromosome.t(), Chromosome.t(), float()) :: {Chromosome.t(), Chromosome.t()}

def crossover(parent1, parent2, swap_rate \\ 0.5) do

{genes1, genes2} =

Enum.zip(parent1.genes, parent2.genes)

|> Enum.map(fn {g1, g2} ->

if :rand.uniform() < swap_rate do

{%{g1 | value: g2.value}, %{g2 | value: g1.value}}

else

{g1, g2}

end

end)

|> Enum.unzip()


child1 = %{parent1 | genes: genes1, fitness: nil, id: Chromosome.generate_id()}

child2 = %{parent2 | genes: genes2, fitness: nil, id: Chromosome.generate_id()}

{child1, child2}

end


@spec mutate(Chromosome.t(), float(), float()) :: Chromosome.t()

def mutate(chromosome, mutation_rate \\ 0.1, sigma \\ 0.1) do

mutated_genes =

Enum.map(chromosome.genes, fn gene ->

if :rand.uniform() < mutation_rate do

mutate_gene(gene, sigma)

else

gene

end

end)


%{chromosome | genes: mutated_genes, fitness: nil}

end


defp mutate_gene(%{type: :float, value: v, min: min, max: max} = gene, sigma) do

range = max - min

perturbation = :rand.normal() sigma range

new_value = clamp(v + perturbation, min, max)

%{gene | value: new_value}

end


defp mutate_gene(%{type: :integer, value: v, min: min, max: max} = gene, sigma) do

range = max - min

perturbation = round(:rand.normal() sigma range)

new_value = clamp(v + perturbation, min, max)

%{gene | value: new_value}

end


defp mutate_gene(%{type: :enum, options: options} = gene, _sigma) do

%{gene | value: Enum.random(options)}

end


defp clamp(value, min, max), do: value |> max(min) |> min(max)

end


Fitness Evaluation


Fitness is computed by applying a candidate configuration to the platform and measuring multiple objectives. The fitness function is a weighted sum of normalized metrics:



defmodule PrismaticMendel.Fitness do

@moduledoc """

Multi-objective fitness evaluation for chromosomes.


Evaluates configurations against platform metrics including

response time, throughput, error rate, and resource usage.

Returns a single fitness score as a weighted sum.

"""


alias PrismaticMendel.Chromosome


@type objective :: %{

name: atom(),

weight: float(),

direction: :minimize | :maximize,

measure: (map() -> float())

}


@objectives [

%{name: :response_time, weight: 0.3, direction: :minimize,

measure: &__MODULE__.measure_response_time/1},

%{name: :throughput, weight: 0.25, direction: :maximize,

measure: &__MODULE__.measure_throughput/1},

%{name: :error_rate, weight: 0.25, direction: :minimize,

measure: &__MODULE__.measure_error_rate/1},

%{name: :resource_usage, weight: 0.2, direction: :minimize,

measure: &__MODULE__.measure_resource_usage/1}

]


@spec evaluate(Chromosome.t()) :: Chromosome.t()

def evaluate(chromosome) do

config = Chromosome.to_config(chromosome)


scores =

Enum.map(@objectives, fn objective ->

raw = objective.measure.(config)

normalized = normalize(raw, objective.direction)

{objective.name, normalized * objective.weight}

end)


total_fitness = scores |> Enum.map(&elem(&1, 1)) |> Enum.sum()


%{chromosome |

fitness: total_fitness,

metadata: Map.put(chromosome.metadata, :scores, Map.new(scores))

}

end


defp normalize(value, :minimize), do: 1.0 / (1.0 + value)

defp normalize(value, :maximize), do: value / (1.0 + value)


@spec measure_response_time(map()) :: float()

def measure_response_time(config) do

# Apply config and measure average response time

PrismaticTelemetry.Metrics.avg_response_time(config)

end


@spec measure_throughput(map()) :: float()

def measure_throughput(config) do

PrismaticTelemetry.Metrics.requests_per_second(config)

end


@spec measure_error_rate(map()) :: float()

def measure_error_rate(config) do

PrismaticTelemetry.Metrics.error_rate(config)

end


@spec measure_resource_usage(map()) :: float()

def measure_resource_usage(config) do

PrismaticTelemetry.Metrics.cpu_memory_composite(config)

end

end


ObjectiveWeightDirectionMetric Source

|-----------|--------|-----------|---------------|

Response Time0.30MinimizeTelemetry avg_response_time Throughput0.25MaximizeTelemetry requests/sec Error Rate0.25MinimizeTelemetry error percentage Resource Usage0.20MinimizeCPU + memory composite

Generation Management


The generation manager orchestrates the evolutionary loop, tracking population statistics and convergence:



defmodule PrismaticMendel.GenerationManager do

@moduledoc """

Manages the evolutionary loop across generations.


Coordinates selection, crossover, mutation, and fitness

evaluation for each generation. Tracks convergence and

terminates when improvement stalls.

"""


use GenServer

require Logger


alias PrismaticMendel.{Chromosome, Operators, Fitness}


@type state :: %{

population: [Chromosome.t()],

generation: non_neg_integer(),

best_fitness: float(),

stall_count: non_neg_integer(),

config: map()

}


@default_config %{

population_size: 50,

elitism_rate: 0.1,

crossover_rate: 0.8,

mutation_rate: 0.1,

max_generations: 100,

stall_limit: 10

}


@spec start_link(keyword()) :: GenServer.on_start()

def start_link(opts) do

GenServer.start_link(__MODULE__, opts, name: __MODULE__)

end


@spec evolve_generation(pid()) :: {:ok, map()} | {:converged, Chromosome.t()}

def evolve_generation(pid \\ __MODULE__) do

GenServer.call(pid, :evolve, 60_000)

end


@impl GenServer

def init(opts) do

config = Map.merge(@default_config, Map.new(opts))

population = initialize_population(config.population_size)


{:ok, %{

population: population,

generation: 0,

best_fitness: 0.0,

stall_count: 0,

config: config

}}

end


@impl GenServer

def handle_call(:evolve, _from, state) do

evaluated = Enum.map(state.population, &Fitness.evaluate/1)

best = Enum.max_by(evaluated, & &1.fitness)


Logger.info(

"Generation #{state.generation}: best=#{Float.round(best.fitness, 4)}, " <>

"avg=#{Float.round(avg_fitness(evaluated), 4)}"

)


new_stall = if best.fitness > state.best_fitness, do: 0, else: state.stall_count + 1


if new_stall >= state.config.stall_limit do

{:reply, {:converged, best}, state}

else

next_population = breed_next_generation(evaluated, state.config)

new_state = %{state |

population: next_population,

generation: state.generation + 1,

best_fitness: max(best.fitness, state.best_fitness),

stall_count: new_stall

}

{:reply, {:ok, generation_stats(evaluated, state.generation)}, new_state}

end

end


defp breed_next_generation(population, config) do

sorted = Enum.sort_by(population, & &1.fitness, :desc)

elite_count = round(length(sorted) * config.elitism_rate)

elite = Enum.take(sorted, elite_count)


children_needed = config.population_size - elite_count


children =

Stream.repeatedly(fn ->

parent1 = Operators.tournament_select(sorted)

parent2 = Operators.tournament_select(sorted)

{child1, child2} = Operators.crossover(parent1, parent2, config.crossover_rate)

[Operators.mutate(child1, config.mutation_rate),

Operators.mutate(child2, config.mutation_rate)]

end)

|> Stream.flat_map(& &1)

|> Enum.take(children_needed)


elite ++ children

end


defp avg_fitness(population) do

population |> Enum.map(& &1.fitness) |> Enum.sum() |> Kernel./(length(population))

end

end


MENDEL enables the platform to self-optimize over time. Configuration parameters that would take engineers weeks to tune through trial-and-error are discovered through evolutionary search. The genetic approach is particularly effective for multi-objective optimization where tradeoffs between competing goals (latency vs throughput, quality vs speed) are not obvious.

Tags

genetic-algorithm evolution mendel optimization elixir

Related Glossary Terms