Skip to content

Examples

Check the git repo examples for a more comprehensive list of examples.


MinSum

Find a set of numbers that sum to the minimum value (0). The solution is represented as a vector of integers, and the fitness function calculates the sum of the integers. The goal is to minimize this sum to 0.

For example, a solution could be:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
import radiate as rd

engine = (
    rd.Engine.int(10, init_range=(0, 100))
    .fitness(lambda x: sum(x))
    .minimizing()
    .select(offspring=rd.Select.elite())
    .alter(rd.Mutate.swap(0.05), rd.Cross.uniform(0.5))
    .limit(rd.Limit.score(0), rd.Limit.generations(1000))
)

result = engine.run()

print(result)
fn minsum() {
    const MIN_SCORE: i32 = 0;

    let engine = GeneticEngine::builder()
        .codec(IntCodec::vector(10, 0..100))
        .minimizing()
        .offspring_selector(EliteSelector::new())
        .mutator(SwapMutator::new(0.05))
        .crossover(UniformCrossover::new(0.5))
        .fitness_fn(|geno: Vec<i32>| geno.iter().sum::<i32>())
        .build();

    let result = engine.run(|epoch| {
        println!("[ {:?} ]: {:?}", epoch.index(), epoch.value());
        epoch.score().as_i32() == MIN_SCORE
    });

    println!("{:?}", result);
}

NQueens

Solve the classic N-Queens problem, where the goal is to place n queens on an n x n board such that no two queens threaten each other. By threatening each other, we mean that they are in the same row, column, or diagonal. The solution is represented as a single chromosome with n genes, where each gene represents the row position of a queen in its respective column. The fitness function calculates the number of pairs of queens that threaten each other, and the goal is to minimize this value to zero.

For example, a solution for n=8 would be:

8-Queens

Use the use_numpy flag to get a numpy.array back when decoding the chromosome for the fitness function. If we use the numba package to compile the fitness function we can actually match the rust example in terms of speed (+/- a few milliseconds).

import numpy as np
import radiate as rd
from numba import jit, uint8

N_QUEENS = 32


@jit(
    uint8(uint8[:]), nopython=True
)  # add this decorator from numba to compile the fitness function to native C code.
def nqueens_fitness_fn(queens: np.ndarray) -> int:
    """Calculate the fitness score for the N-Queens problem."""

    i_indices, j_indices = np.triu_indices(N_QUEENS, k=1)

    same_row = queens[i_indices] == queens[j_indices]

    same_diagonal = np.abs(i_indices - j_indices) == np.abs(
        queens[i_indices] - queens[j_indices]
    )

    return np.sum(same_row) + np.sum(same_diagonal)


engine = (
    rd.Engine.int(N_QUEENS, init_range=(0, N_QUEENS), use_numpy=True, dtype=rd.UInt8)
    .fitness(nqueens_fitness_fn)
    .minimizing()
    .alter(
        rd.Cross.multipoint(0.75, 2),
        rd.Mutate.uniform(0.05),
    )
    .limit(rd.Limit.score(0), rd.Limit.generations(1000))
)

result = engine.run(log=False)
print(result)

board = result.value()
for i in range(N_QUEENS):
    for j in range(N_QUEENS):
        if board[j] == i:
            print("Q ", end="")
        else:
            print(". ", end="")
    print()
const N_QUEENS: usize = 45;

fn nqueens() {
    random_provider::seed(12345);

    let engine = GeneticEngine::builder()
        .codec(IntChromosome::from((N_QUEENS, 0..N_QUEENS as i8)))
        .minimizing()
        .offspring_selector(BoltzmannSelector::new(4.0))
        .crossover(MultiPointCrossover::new(0.75, 2))
        .mutator(UniformMutator::new(0.05))
        .fitness_fn(|queens: Vec<i8>| {
            let mut score = 0;

            for i in 0..N_QUEENS {
                for j in (i + 1)..N_QUEENS {
                    if queens[i] == queens[j] {
                        score += 1;
                    }
                    if (i as i8 - j as i8).abs() == (queens[i] - queens[j]).abs() {
                        score += 1;
                    }
                }
            }

            score
        })
        .build();

    let result = engine.iter().logging().until_score(0).last().unwrap();

    println!("Best Score: {:?}", result);
    println!("\nResult Queens Board ({:.3?}):", result.time());

    let board = &result.value();
    for i in 0..N_QUEENS {
        for j in 0..N_QUEENS {
            if board[j] == i as i8 {
                print!("Q ");
            } else {
                print!(". ");
            }
        }
        println!();
    }
}

Rastrigin

The Rastrigin function is a non-convex function used as a benchmark test problem for optimization algorithms. The function is highly multimodal, with many local minima, making it challenging for optimization algorithms to find the global minimum. It is defined as: $$ f(x) = A \cdot n + \sum_{i=1}^{n} \left[ x_i^2 - A \cdot \cos(2 \pi x_i) \right] $$ where:

  • \( A \) is a constant (typically set to 10)
  • \( n \) is the number of dimensions (in this case 2)
  • \( x_i \) are the input variables.
  • The global minimum occurs at \( x = 0 \) for all dimensions, where the function value is \( 0 \).
Rastrigin
import math

import radiate as rd

A = 10.0
RANGE = 5.12
N_GENES = 2


def rastrigin_fitness_fn(x: list[float]) -> float:
    value = A * N_GENES
    for i in range(N_GENES):
        value += x[i] ** 2 - A * math.cos(2.0 * 3.141592653589793 * x[i])
    return value


engine = (
    rd.Engine.float(2, init_range=(-RANGE, RANGE), bounds=(-10.0, 10.0))
    .fitness(rastrigin_fitness_fn)
    .minimizing()
    .alter(rd.Cross.uniform(0.5), rd.Mutate.arithmetic(0.01))
    .limit(rd.Limit.score(0.0001))
)

print(engine.run())
fn rastrigin() {
    const MIN_SCORE: f32 = 0.00;
    const MAX_SECONDS: f64 = 1.0;
    const A: f32 = 10.0;
    const RANGE: f32 = 5.12;
    const N_GENES: usize = 2;

    let engine = GeneticEngine::builder()
        .codec(FloatCodec::vector(N_GENES, -RANGE..RANGE))
        .minimizing()
        .population_size(500)
        .alter(alters!(
            UniformCrossover::new(0.5),
            ArithmeticMutator::new(0.01)
        ))
        .fitness_fn(move |genotype: Vec<f32>| {
            let mut value = A * N_GENES as f32;
            for i in 0..N_GENES {
                value += genotype[i].powi(2) - A * (2.0 * std::f32::consts::PI * genotype[i]).cos();
            }

            value
        })
        .build();

    let result = engine.run(|ctx| {
        println!("[ {:?} ]: {:?}", ctx.index(), ctx.score().as_f32());
        ctx.score().as_f32() <= MIN_SCORE || ctx.seconds() > MAX_SECONDS
    });

    println!("{:?}", result);
}

DTLZ1

The DTLZ1 problem is a well-known multiobjective optimization problem that is used to test the performance of multiobjective optimization algorithms. It is a 3-objective problem with 4 variables and is defined as:

\[ \begin{align*} \text{minimize} \quad & f_1(x) = (1 + g) \cdot x_1 \cdot x_2 \\ \text{minimize} \quad & f_2(x) = (1 + g) \cdot x_1 \cdot (1 - x_2) \\ \text{minimize} \quad & f_3(x) = (1 + g) \cdot (1 - x_1) \\ \text{subject to} \quad & 0 \leq x_i \leq 1 \quad \text{for} \quad i = 1, 2, 3, 4 \\ \text{where} \quad & g = \sum_{i=3}^{4} (x_i - 0.5)^2 \end{align*} \]

Again here we are using the numba crate to compile the fitness function down to native C - once again, this allows us to match the same speed as rust.

import numpy as np
import plotly.graph_objects as go
import radiate as rd
from numba import float32, jit

rd.random.seed(501)

variables = 4
objectives = 3
k = variables - objectives + 1


# Because we are using numpy arrays, we can use numba to compile this function to native code for speed.
# This allows us to match the speed of the rust implementation.
@jit(float32[:](float32[:]), nopython=True)
def dtlz1_fitness_fn(val: np.ndarray) -> np.ndarray:
    g_vals = val[variables - k :] - 0.5
    g = 100.0 * (k + np.sum(g_vals**2 - np.cos(20.0 * np.pi * g_vals)))

    base = 0.5 * (1.0 + g)

    f = np.full(objectives, base, dtype=np.float32)

    for i in range(objectives):
        prod_end = objectives - 1 - i
        if prod_end > 0:
            f[i] *= np.prod(val[:prod_end])

        if i > 0:
            f[i] *= 1.0 - val[objectives - 1 - i]

    return f


engine = (
    rd.Engine.float(variables, use_numpy=True, dtype=rd.Float32)
    .fitness(dtlz1_fitness_fn)
    .objective(rd.MIN, rd.MIN, rd.MIN)
    .front_range(100, 150)
    # NSGA-III for 3+ objectives: crowded-comparison tournament for parents, reference-point
    # niching for survivors. A lower offspring fraction keeps more of the evaluated front.
    .select(
        rd.Select.tournament_nsga2(),
        rd.Select.nsga3(points=12),
        frac=0.5,
    )
    .alter(
        rd.Cross.sbx(0.8, 20.0),  # <- Simulated Binary Crossover
        rd.Mutate.polynomial(0.1, 20.0),  # <- Polynomial Mutation
    )
    .limit(rd.Limit.generations(2000))
)
result = engine.run(ui=True)

# When running an MO problem, we can get the resulting pareto from from the
# engine's epoch result. This is stored in the 'front()' field of the result here:
front = result.front()

x = [member.score()[0] for member in front]
y = [member.score()[1] for member in front]
z = [member.score()[2] for member in front]

fig = go.Figure(go.Scatter3d(x=x, y=y, z=z, mode="markers"))
fig.update_layout(
    scene={
        "xaxis": {"range": [0, 0.5]},
        "yaxis": {"range": [0, 0.5]},
        "zaxis": {"range": [0, 0.5]},
    }
)
fig.show()
fn dtlz1() {
    const VARIABLES: usize = 4;
    const OBJECTIVES: usize = 3;
    const K: usize = VARIABLES - OBJECTIVES + 1;

    fn dtlz_1(values: &[f32]) -> Vec<f32> {
        let mut g = 0.0;
        for i in VARIABLES - K..VARIABLES {
            g +=
                (values[i] - 0.5).powi(2) - (20.0 * std::f32::consts::PI * (values[i] - 0.5)).cos();
        }

        g = 100.0 * (K as f32 + g);

        let mut f = vec![0.0; OBJECTIVES];
        for i in 0..OBJECTIVES {
            f[i] = 0.5 * (1.0 + g);
            for j in 0..OBJECTIVES - 1 - i {
                f[i] *= values[j];
            }

            if i != 0 {
                f[i] *= 1.0 - values[OBJECTIVES - 1 - i];
            }
        }

        f
    }

    let codec = FloatCodec::vector(VARIABLES, 0_f32..1_f32);

    let engine = GeneticEngine::builder()
        .codec(codec)
        .multi_objective(vec![Optimize::Minimize; OBJECTIVES])
        // NSGA-III for 3+ objectives: crowded-comparison tournament for parents,
        // reference-point niching for survivors.
        .offspring_selector(TournamentNSGA2Selector::new())
        .survivor_selector(NSGA3Selector::new(12))
        // A lower offspring fraction keeps more of the already-evaluated front each generation.
        .offspring_fraction(0.5)
        .alter(alters!(
            SimulatedBinaryCrossover::new(0.8_f32, 20.0),
            PolynomialMutator::new(0.1, 20.0),
        ))
        .fitness_fn(|geno: Vec<f32>| dtlz_1(&geno))
        .build();

    let result = engine
        .run(|ctx| {
            println!("[ {:?} ]", ctx.index());
            ctx.index() > 1000
        })
        .unwrap();

    // When running an MO problem, we can get the resulting pareto from from the
    // engine's epoch result. This is stored in the 'front()' field of the result here:
    let front = result.front();
}

The resulting Pareto front can be visualized using Plotly, as shown below:


Graph - XOR Problem

Evolve a Graph<Op<f32>> to solve the XOR problem (NeuroEvolution).

import radiate as rd

inputs = [[0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 1.0]]
answers = [[0.0], [0.0], [1.0], [1.0]]

codec = rd.GraphCodec.directed(
    shape=(2, 1),
    vertex=[rd.Op.add(), rd.Op.mul(), rd.Op.linear()],
    edge=rd.Op.weight(),
    output=rd.Op.linear(),
)

engine = (
    rd.Engine(codec)
    .regression(inputs, answers, loss=rd.MSE)
    .alter(
        rd.Cross.graph(0.5, 0.5),
        rd.Mutate.op(0.07, 0.05),
        rd.Mutate.graph(0.1, 0.1),
    )
    .limit(rd.Limit.score(0.001), rd.Limit.generations(1000))
)

result = engine.run(log=True)

for input, target in zip(inputs, answers):
    print(f"Input: {input}, Target: {target}, Output: {result.value().eval([input])}")

Requires gp feature flag

fn graph_xor() {
    const MAX_INDEX: i32 = 500;
    const MIN_SCORE: f32 = 0.01;

    random_provider::seed(501);

    let store = vec![
        (NodeType::Input, vec![Op::var(0), Op::var(1)]),
        (NodeType::Edge, vec![Op::weight(), Op::identity()]),
        (NodeType::Vertex, ops::all_ops()),
        (NodeType::Output, vec![Op::sigmoid()]),
    ];

    let graph_codec = GraphCodec::directed(2, 1, store);
    let regression = Regression::new(get_dataset(), Loss::MSE);

    let engine = GeneticEngine::builder()
        .codec(graph_codec)
        .fitness_fn(regression)
        .minimizing()
        .alter(alters!(
            GraphCrossover::new(0.5, 0.5),
            OperationMutator::new(0.05, 0.05),
            GraphMutator::new(0.06, 0.01).allow_recurrent(false),
        ))
        .build();

    // Using the engine iterator
    engine
        .iter()
        .logging()
        .until_score(MIN_SCORE)
        .last()
        .inspect(display)
        .expect("No result from engine run");

    fn display(result: &Generation<GraphChromosome<Op<f32>>, Graph<Op<f32>>>) {
        let mut reducer = GraphEvaluator::new(result.value());
        for sample in get_dataset().iter() {
            let output = &reducer.eval_mut(&sample.0)[0];
            println!(
                "{:?} -> expected: {:?}, actual: {:.3?}",
                sample.0, sample.1, output
            );
        }

        println!("{result:?}");
    }

    fn get_dataset() -> DataSet<f32> {
        let inputs = vec![
            vec![0.0, 0.0],
            vec![1.0, 1.0],
            vec![1.0, 0.0],
            vec![0.0, 1.0],
        ];

        let answers = vec![vec![0.0], vec![0.0], vec![1.0], vec![1.0]];

        DataSet::new(inputs, answers)
    }
}

Tree - Regression

Evolve a Tree<Op<f32>> to solve the a regression problem (Genetic Programming).

import radiate as rd

inputs = [[0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 1.0]]
answers = [[0.0], [0.0], [1.0], [1.0]]

codec = rd.TreeCodec(
    shape=(2, 1),
    vertex=[rd.Op.sub(), rd.Op.mul(), rd.Op.add()],
    root=rd.Op.linear(),
)

engine = (
    rd.Engine(codec)
    .regression(inputs, answers, loss=rd.MSE)
    .alter(rd.Cross.tree(0.7), rd.Mutate.hoist(0.01))
    .limit(rd.Limit.score(0.01), rd.Limit.seconds(1))
)


result = engine.run(log=True)
print(result)

for input, target in zip(inputs, answers):
    print(f"Input: {input}, Target: {target}, Output: {result.value().eval([input])}")

Requires gp feature flag

fn tree() {
    const MIN_SCORE: f32 = 0.01;
    const MAX_SECONDS: f64 = 1.0;

    random_provider::seed(518);

    let store = vec![
        (NodeType::Vertex, vec![Op::add(), Op::sub(), Op::mul()]),
        (NodeType::Leaf, vec![Op::var(0)]),
    ];

    let tree_codec = TreeCodec::single(3, store).constraint(|root| root.size() < 30);
    let regression = Regression::new(get_dataset(), Loss::MSE);

    let engine = GeneticEngine::builder()
        .codec(tree_codec)
        .fitness_fn(regression)
        .minimizing()
        .mutator(HoistMutator::new(0.01))
        .crossover(TreeCrossover::new(0.7))
        .build();

    let result = engine
        .run(|ctx| {
            println!("[ {:?} ]: {:?}", ctx.index(), ctx.score().as_f32());
            ctx.score().as_f32() < MIN_SCORE || ctx.seconds() > MAX_SECONDS
        })
        .unwrap();

    display(&result);

    fn display(result: &Generation<TreeChromosome<Op<f32>>, Tree<Op<f32>>>) {
        Accuracy::default()
            .named("Regression Tree")
            .on(&get_dataset())
            .loss(Loss::MSE)
            .eval(result.value())
            .inspect(|acc| {
                println!("{}", result.metrics().dashboard());
                println!("Best Tree: {}", result.value().format());
                println!("{:?}", acc);
            });
    }

    fn get_dataset() -> DataSet<f32> {
        let mut inputs = Vec::new();
        let mut answers = Vec::new();

        let mut input = -1.0;
        for _ in -10..10 {
            input += 0.1;
            inputs.push(vec![input]);
            answers.push(vec![compute(input)]);
        }

        DataSet::new(inputs, answers)
    }

    fn compute(x: f32) -> f32 {
        4.0 * x.powf(3.0) - 3.0 * x.powf(2.0) + x
    }
}