Genetic programming for .NET

Evolve programs,
not just parameters.

Write the small methods your bot is allowed to call. GeneCodeCS breeds random programs from them, compiles each one in memory, scores it with your fitness function, and lets the best ones reproduce.

# .NET 10 SDK (library targets .NET 8 and 10)
git clone https://github.com/EddPorter/GeneCodeCS
cd GeneCodeCS && dotnet test
.NET 8 & 10GPL-3.0-or-laterNo code generation to diskReproducible with a seed
Live demo

A population, evolving

This is a small working model of what the library does, running in your browser. Each card is one bot’s program drawn as a tree. Press play, or step through a generation one stage at a time.

The task: a counter starts at 0. Each bot’s program runs 20 times. The bot is fitter the closer the counter finishes to 10 (fitness = 1000 − distance). The building blocks are Increment(n), Decrement(n) and the question BelowTarget(). No bot is told how to combine them.

Fitness by generation

Best bot’s counter over its 20 runs

The best bot, as C#

The demo is a miniature written in JavaScript for this page. It uses the same ingredients as the library (tree genomes, tournament selection, subtree crossover, mutation, elitism and random newcomers) but isn’t the library itself. The C# below is.

How it works

Four steps, one loop

You supply the vocabulary and the scoring. The library does the evolving.

Describe

Public void methods on your bot become actions. Public bool methods become decisions. Parameters are evolvable constants you control the range of.

Grow

The first generation is random expression trees built from those parts, with Grow or Full initialisation up to a depth you choose.

Compile

Each tree is turned into a delegate with System.Linq.Expressions, in memory. There are no files to write and no assemblies to leak.

Select & breed

Your Evaluate sets Fitness. Fitter bots are more likely to be parents; subtrees are swapped, nodes occasionally mutated, and the loop repeats.

The code

This is the whole bot

The demo above is this C# class. Nothing else describes the problem.

public class CounterBot : BaseBot
{
  private int _steps;
  public int Counter { get; private set; }

  // Actions: public void methods1
  public void Increment(StepParameter s)2
  { Counter += s.Value; }
  public void Decrement(StepParameter s)
  { Counter -= s.Value; }

  // Decisions: public bool methods3
  public bool BelowTarget() { return Counter < 10; }

  protected override bool NotFinished()4
  { return ++_steps <= 20; }

  protected override void Evaluate()5
  { Fitness = 1000 - Math.Abs(10 - Counter); }
}

// An evolvable constant, kept to the range 1 to 3
public class StepParameter : IParameter<int>
{
  public StepParameter(Random r)6
  { Value = r.Next(1, 4); }
  public StepParameter(int value)
  { Value = value; }
  public int Value { get; private set; }
}

// Evolve
var population = new Population<CounterBot>();
var best = population.SimulateGenerations<object>(null,
    generationLimit: 30, botsPerGeneration: 60,
    fitnessThreshold: 999);7
  1. 1

    Your methods are the gene pool. The library finds them by reflection. void methods become the leaves of a program tree.

  2. 2

    Parameters are part of the genome. Each Increment in a tree carries its own evolved step size.

  3. 3

    Decisions make branches. A bool method becomes an if/else with a subtree on each side.

  4. 4

    You say when a bot is done. The evolved program runs in a loop until NotFinished returns false.

  5. 5

    You define “good”. Set Fitness from whatever the bot did. Higher is better. A bot that throws is scored int.MinValue.

  6. 6

    Two constructors. One takes a Random to pick the starting value, so you control the range. The other restores a stored value when the tree is compiled.

  7. 7

    One call evolves it. You get back the best bot or bots, each with its tree, name, parents and compiled instance.

You writeThe library treats it asShown in the demo as
public void Increment(StepParameter step)A terminal gene (a leaf), with one evolvable parameterA green +n node
public bool BelowTarget()A branch gene: if / elseA blue Below? node with yes and no subtrees
Several genes in a rowA sequence, run top to bottomA grey then node
Evaluate()The fitness functionThe bar on each card
Control

The knobs that matter

SettingWhat it doesDefault
botsPerGeneration, generationLimit, maxTreeDepthPopulation size, how long to run, how large the first random programs can be30, 20, 3
fitnessThresholdStop early once a bot beats this fitnessNever
InitialisationMethodGrow mixes functions and terminals; Full fills every levelGrow
SelectionMethodTournament, or FitnessProportionate (chance of parenthood in proportion to fitness)Tournament
CrossoverRate, MutationRateChance, 0–100, of swapping subtrees or mutating during breeding90, 5
RandomBotPercentageShare of each new generation made of fresh random bots, to keep diversity up20
starterBotsSeed the first generation with programs you already have, or the output of an earlier runNone
Optimise callbackSimplify each new tree (for example, remove impossible branches) before it is compiledNone
Random seedMake a run reproducibleTime-based
Is it for you?

Good at some things, not all

A good fit

  • Behaviour-style programs: trading rules, game agents, simulation controllers, scheduling heuristics.
  • Problems you can score automatically and cheaply, thousands of times.
  • When you want to read the answer. The result is a small program, not a weight matrix.
  • Prototyping: if you can write the building blocks, you can start evolving in an afternoon.

Think twice

  • Numeric symbolic regression. The building blocks are bool and void methods, not typed expressions that return numbers.
  • Noisy or thin data. Evolution will happily overfit; score on held-out data.
  • Expensive fitness. Each bot in each generation must be run, so slow evaluation multiplies fast.
  • Anything needing a guarantee. Evolution gives you good candidates, not proofs.