Skip to content

API Reference

Auto-generated from the package's numpy-style docstrings.

Algorithms

nsga2

NSGA-II algorithm with pluggable strategies.

Examples:

>>> import numpy as np
>>> from ctrl_freak.algorithms.nsga2 import nsga2
>>> def init(rng):
...     return rng.uniform(0.0, 1.0, size=3)
>>> def evaluate(x):
...     return np.array([np.sum(x), np.sum(1.0 - x)])
>>> result = nsga2(
...     init=init,
...     evaluate=evaluate,
...     crossover=lambda p1, p2: (p1 + p2) / 2,
...     mutate=lambda x: np.clip(x + 0.01, 0.0, 1.0),
...     pop_size=10,
...     n_generations=2,
...     seed=42,
... )
>>> result.population.x.shape
(10, 3)

nsga2

nsga2(
    init: Callable[[Generator], ndarray],
    evaluate: Callable[[ndarray], ndarray],
    crossover: Callable[[ndarray, ndarray], ndarray],
    mutate: Callable[[ndarray], ndarray],
    pop_size: int,
    n_generations: int,
    seed: int | None = None,
    callback: Callable[[NSGA2Result, int], bool]
    | None = None,
    select: str | ParentSelector = "crowded",
    survive: str | SurvivorSelector = "nsga2",
    n_workers: int = 1,
    evaluate_batch: Callable[[ndarray], ndarray]
    | None = None,
) -> NSGA2Result

Run NSGA-II multi-objective optimization.

Parameters:

Name Type Description Default
init Callable[[Generator], ndarray]

Callable that initializes one individual from a random generator.

required
evaluate Callable[[ndarray], ndarray]

Callable that evaluates one individual and returns objective values.

required
crossover Callable[[ndarray, ndarray], ndarray]

Callable that crosses two parents to produce one child.

required
mutate Callable[[ndarray], ndarray]

Callable that mutates one individual.

required
pop_size int

Population size. Must be positive and even.

required
n_generations int

Number of generations to run.

required
seed int | None

Master random seed. If None, system entropy is used.

None
callback Callable[[NSGA2Result, int], bool] | None

Optional callback called before each generation. Return True to stop.

None
select str | ParentSelector

Parent selection strategy name or callable.

'crowded'
survive str | SurvivorSelector

Survivor selection strategy name or callable.

'nsga2'
n_workers int

Number of workers for objective evaluation. Parallel evaluation is deterministic only when evaluate is pure.

1
evaluate_batch Callable[[ndarray], ndarray] | None

Optional whole-population objective. When provided, it receives the entire (pop_size, n_params) population matrix in a single call and returns the objective matrix of shape (pop_size, n_obj). This bypasses the per-individual evaluate / lift loop entirely, so evaluate is not called. When None (default), evaluation falls back to the per-individual evaluate path and the result is unchanged.

None

Returns:

Type Description
NSGA2Result

Final population, ranks, crowding distances, generation count, and evaluation count.

Raises:

Type Description
ValueError

If size, generation, or worker arguments are invalid.

KeyError

If a named strategy is not registered.

Examples:

>>> import numpy as np
>>> from ctrl_freak.algorithms.nsga2 import nsga2
>>> def init(rng):
...     return rng.uniform(0.0, 1.0, size=2)
>>> def evaluate(x):
...     return np.array([np.sum(x), np.sum(1.0 - x)])
>>> result = nsga2(
...     init=init,
...     evaluate=evaluate,
...     crossover=lambda p1, p2: (p1 + p2) / 2,
...     mutate=lambda x: x.copy(),
...     pop_size=10,
...     n_generations=2,
...     seed=1,
... )
>>> result.generations
2
>>> # Whole-population evaluation via evaluate_batch (returns (pop_size, n_obj)):
>>> def evaluate_batch(pop):
...     return np.stack([pop.sum(axis=1), (1.0 - pop).sum(axis=1)], axis=1)
>>> batched = nsga2(
...     init=init,
...     evaluate=evaluate,
...     crossover=lambda p1, p2: (p1 + p2) / 2,
...     mutate=lambda x: x.copy(),
...     pop_size=10,
...     n_generations=2,
...     seed=1,
...     evaluate_batch=evaluate_batch,
... )
>>> batched.population.x.shape
(10, 2)

ga

Standard genetic algorithm with pluggable strategies.

Examples:

>>> import numpy as np
>>> from ctrl_freak.algorithms.ga import ga
>>> def init(rng):
...     return rng.uniform(0.0, 1.0, size=3)
>>> def evaluate(x):
...     return float(np.sum(x**2))
>>> result = ga(
...     init=init,
...     evaluate=evaluate,
...     crossover=lambda p1, p2: (p1 + p2) / 2,
...     mutate=lambda x: np.clip(x + 0.01, 0.0, 1.0),
...     pop_size=10,
...     n_generations=2,
...     seed=42,
... )
>>> result.population.x.shape
(10, 3)

ga

ga(
    init: Callable[[Generator], ndarray],
    evaluate: Callable[[ndarray], float],
    crossover: Callable[[ndarray, ndarray], ndarray],
    mutate: Callable[[ndarray], ndarray],
    pop_size: int,
    n_generations: int,
    seed: int | None = None,
    callback: Callable[[GAResult, int], bool] | None = None,
    select: str | ParentSelector = "tournament",
    survive: str | SurvivorSelector = "elitist",
    n_workers: int = 1,
    evaluate_batch: Callable[[ndarray], ndarray]
    | None = None,
) -> GAResult

Run a single-objective genetic algorithm.

Parameters:

Name Type Description Default
init Callable[[Generator], ndarray]

Callable that initializes one individual from a random generator.

required
evaluate Callable[[ndarray], float]

Callable that evaluates one individual and returns a scalar objective. Lower objective values are better.

required
crossover Callable[[ndarray, ndarray], ndarray]

Callable that crosses two parents to produce one child.

required
mutate Callable[[ndarray], ndarray]

Callable that mutates one individual.

required
pop_size int

Population size. Must be positive and even.

required
n_generations int

Number of generations to run.

required
seed int | None

Master random seed. If None, system entropy is used.

None
callback Callable[[GAResult, int], bool] | None

Optional callback called before each generation. Return True to stop.

None
select str | ParentSelector

Parent selection strategy name or callable.

'tournament'
survive str | SurvivorSelector

Survivor selection strategy name or callable.

'elitist'
n_workers int

Number of workers for objective evaluation. Parallel evaluation is deterministic only when evaluate is pure.

1
evaluate_batch Callable[[ndarray], ndarray] | None

Optional whole-population objective. When provided, it receives the entire (pop_size, n_params) population matrix in a single call and returns the objective for every individual with shape (pop_size,) or (pop_size, 1). This bypasses the per-individual evaluate / lift loop entirely, so evaluate is not called. When None (default), evaluation falls back to the per-individual evaluate path and the result is byte-for-byte identical to prior releases.

None

Returns:

Type Description
GAResult

Final population, fitness vector, best index, generation count, and evaluation count.

Raises:

Type Description
ValueError

If size, generation, or worker arguments are invalid.

KeyError

If a named strategy is not registered.

Examples:

>>> import numpy as np
>>> from ctrl_freak.algorithms.ga import ga
>>> def init(rng):
...     return rng.uniform(0.0, 1.0, size=2)
>>> def evaluate(x):
...     return float(np.sum(x**2))
>>> result = ga(
...     init=init,
...     evaluate=evaluate,
...     crossover=lambda p1, p2: (p1 + p2) / 2,
...     mutate=lambda x: x.copy(),
...     pop_size=10,
...     n_generations=2,
...     seed=1,
... )
>>> result.generations
2
>>> # Whole-population evaluation via evaluate_batch (bypasses the per-individual loop):
>>> def evaluate_batch(pop):
...     return np.sum(pop**2, axis=1)
>>> batched = ga(
...     init=init,
...     evaluate=evaluate,
...     crossover=lambda p1, p2: (p1 + p2) / 2,
...     mutate=lambda x: x.copy(),
...     pop_size=10,
...     n_generations=2,
...     seed=1,
...     evaluate_batch=evaluate_batch,
... )
>>> batched.population.x.shape
(10, 2)

Result types

NSGA2Result dataclass

NSGA2Result(
    population: Population,
    rank: ndarray,
    crowding_distance: ndarray,
    generations: int,
    evaluations: int,
)

Results from NSGA-II multi-objective optimization algorithm.

This class encapsulates the final population along with algorithm-specific metadata like Pareto ranks and crowding distances. All arrays are copied on construction to ensure immutability.

Attributes:

Name Type Description
population Population

The final population after optimization.

rank ndarray

Pareto rank for each individual. Rank 0 indicates individuals on the Pareto front.

crowding_distance ndarray

Crowding distance for each individual.

generations int

Number of generations completed during optimization.

evaluations int

Total number of objective function evaluations performed.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.results import NSGA2Result
>>> x = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
>>> obj = np.array([[0.5, 0.5], [0.3, 0.7], [0.4, 0.6]])
>>> pop = Population(x=x, objectives=obj)
>>> result = NSGA2Result(
...     population=pop,
...     rank=np.array([0, 0, 1]),
...     crowding_distance=np.array([np.inf, np.inf, 0.5]),
...     generations=100,
...     evaluations=5000,
... )
>>> len(result.pareto_front)
2

pareto_front property

pareto_front: Population

Extract the Pareto front (rank-0 individuals) as a new Population.

Returns:

Type Description
Population

A new Population containing only individuals with rank 0.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.results import NSGA2Result
>>> pop = Population(
...     x=np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]),
...     objectives=np.array([[0.5, 0.5], [0.3, 0.7], [0.4, 0.6]]),
... )
>>> result = NSGA2Result(
...     population=pop,
...     rank=np.array([0, 0, 1]),
...     crowding_distance=np.array([np.inf, np.inf, 0.5]),
...     generations=1,
...     evaluations=3,
... )
>>> len(result.pareto_front)
2

GAResult dataclass

GAResult(
    population: Population,
    fitness: ndarray,
    best_idx: int,
    generations: int,
    evaluations: int,
)

Results from single-objective genetic algorithm optimization.

This class encapsulates the final population along with fitness values for each individual. In minimization problems, lower fitness is better. All arrays are copied on construction to ensure immutability.

Attributes:

Name Type Description
population Population

The final population after optimization.

fitness ndarray

Fitness values for each individual. Lower values indicate better fitness for minimization problems.

best_idx int

Index of the best individual.

generations int

Number of generations completed during optimization.

evaluations int

Total number of objective function evaluations performed.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.results import GAResult
>>> pop = Population(x=np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]))
>>> result = GAResult(
...     population=pop,
...     fitness=np.array([0.5, 0.3, 0.7]),
...     best_idx=1,
...     generations=50,
...     evaluations=2500,
... )
>>> result.best[1]
0.3

best property

best: tuple[ndarray, float]

Extract the best individual and its fitness value.

Returns:

Type Description
tuple[ndarray, float]

Decision variables and fitness value for the best individual.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.results import GAResult
>>> pop = Population(x=np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]))
>>> result = GAResult(
...     population=pop,
...     fitness=np.array([0.5, 0.3, 0.7]),
...     best_idx=1,
...     generations=1,
...     evaluations=3,
... )
>>> best_x, best_fitness = result.best
>>> best_x
array([3., 4.])
>>> best_fitness
0.3

Data structures

Population dataclass

Population(x: ndarray, objectives: ndarray | None = None)

Immutable struct-of-arrays representation of a population.

This class stores multiple individuals using a struct-of-arrays layout for efficient vectorized operations. All arrays are copied on construction to ensure immutability.

Population is algorithm-agnostic - it only stores decision variables and objective values. Algorithm-specific data (like Pareto ranks or crowding distances) should be managed separately by the algorithm implementation.

Attributes:

Name Type Description
x ndarray

Decision variables for all individuals. Shape is (n, n_vars).

objectives ndarray | None

Objective values. Shape is (n, n_obj), or None if not evaluated.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> x = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
>>> obj = np.array([[0.5, 0.5], [0.3, 0.7], [0.4, 0.6]])
>>> pop = Population(x=x, objectives=obj)
>>> len(pop)
3
>>> pop.n_vars
2
>>> pop.n_obj
2

n_individuals property

n_individuals: int

Return the number of individuals in the population.

Returns:

Type Description
int

Number of individuals.

n_vars property

n_vars: int

Return the number of decision variables per individual.

Returns:

Type Description
int

Number of decision variables.

n_obj property

n_obj: int | None

Return the number of objectives, or None if not evaluated.

Returns:

Type Description
int | None

Number of objectives, or None if objectives is None.

IndividualView dataclass

IndividualView(x: ndarray, objectives: ndarray | None)

Read-only view of a single individual in a population.

This class provides a convenient way to access the data for a single individual, returned by Population.getitem.

Attributes:

Name Type Description
x ndarray

Decision variables for this individual.

objectives ndarray | None

Objective values for this individual, or None.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> pop = Population(x=np.array([[1.0, 2.0], [3.0, 4.0]]))
>>> individual = pop[0]
>>> individual.x
array([1., 2.])

Genetic operators

sbx_crossover

sbx_crossover(
    eta: float = 15.0,
    bounds: Bounds = (0.0, 1.0),
    seed: int | None = None,
) -> Callable[[np.ndarray, np.ndarray], np.ndarray]

Create a bounded Simulated Binary Crossover operator.

Parameters:

Name Type Description Default
eta float

Distribution index. Higher values produce children closer to parents.

15.0
bounds tuple

Lower and upper decision-variable bounds. Scalars apply to all variables; arrays provide per-variable bounds.

(0.0, 1.0)
seed int

Random seed for the standalone generator used until set_rng is called.

None

Returns:

Type Description
Callable

Callable with signature (p1, p2) -> child. The returned object also exposes set_rng(rng) for algorithm-level seed injection.

References

Deb, K., & Agrawal, R. B. (1995). Simulated binary crossover for continuous search space. Complex Systems, 9(2), 115-148.

Examples:

>>> crossover = sbx_crossover(eta=15.0, bounds=(0.0, 1.0), seed=42)
>>> p1 = np.array([0.2, 0.4, 0.6])
>>> p2 = np.array([0.3, 0.5, 0.7])
>>> child = crossover(p1, p2)
>>> child.shape
(3,)

Per-variable bounds:

>>> lower = np.array([0.0, -10.0, 100.0])
>>> upper = np.array([1.0, 10.0, 200.0])
>>> crossover = sbx_crossover(eta=15.0, bounds=(lower, upper), seed=42)
>>> crossover(np.array([0.5, 0.0, 150.0]), np.array([0.8, -5.0, 180.0])).shape
(3,)

Seed injection:

>>> cx = sbx_crossover(eta=15.0, bounds=(0.0, 1.0), seed=0)
>>> cx.set_rng(np.random.default_rng(123))
>>> child = cx(np.array([0.2, 0.4]), np.array([0.6, 0.8]))
>>> child.shape
(2,)

polynomial_mutation

polynomial_mutation(
    eta: float = 20.0,
    prob: float | None = None,
    bounds: Bounds = (0.0, 1.0),
    seed: int | None = None,
) -> Callable[[np.ndarray], np.ndarray]

Create a bounded polynomial mutation operator.

Parameters:

Name Type Description Default
eta float

Distribution index. Higher values produce smaller perturbations.

20.0
prob float

Mutation probability per variable. If omitted, uses 1 / n_vars.

None
bounds tuple

Lower and upper decision-variable bounds. Scalars apply to all variables; arrays provide per-variable bounds.

(0.0, 1.0)
seed int

Random seed for the standalone generator used until set_rng is called.

None

Returns:

Type Description
Callable

Callable with signature x -> x'. The returned object also exposes set_rng(rng) for algorithm-level seed injection.

References

Deb, K., & Goyal, M. (1996). A combined genetic adaptive search (GeneAS) for engineering design. Computer Science and Informatics, 26(4), 30-45.

Examples:

>>> mutate = polynomial_mutation(eta=20.0, prob=0.1, bounds=(0.0, 1.0), seed=42)
>>> x = np.array([0.5, 0.5, 0.5])
>>> mutated = mutate(x)
>>> mutated.shape
(3,)

Per-variable bounds:

>>> lower = np.array([0.0, -10.0, 100.0])
>>> upper = np.array([1.0, 10.0, 200.0])
>>> mutate = polynomial_mutation(eta=20.0, prob=0.1, bounds=(lower, upper), seed=42)
>>> mutate(np.array([0.5, 0.0, 150.0])).shape
(3,)

lift

lift(
    fn: Callable[[ndarray], ndarray],
) -> Callable[[np.ndarray], np.ndarray]

Lift a per-individual function to work on a population.

This utility allows users to write simple per-individual functions while the framework handles batching/vectorization.

Parameters:

Name Type Description Default
fn Callable

Function that operates on one individual with signature (n_vars,) -> (n_out,).

required

Returns:

Type Description
Callable

Function that operates on a population with signature (n, n_vars) -> (n, n_out).

Examples:

>>> def evaluate_one(x: np.ndarray) -> np.ndarray:
...     return np.array([x.sum(), x.prod()])
>>> evaluate = lift(evaluate_one)
>>> pop_x = np.array([[1.0, 2.0], [3.0, 4.0]])
>>> evaluate(pop_x)
array([[ 3.,  2.],
       [ 7., 12.]])

lift_parallel

lift_parallel(
    fn: Callable[[ndarray], ndarray], n_workers: int
) -> Callable[[np.ndarray], np.ndarray]

Lift a per-individual function to work on a population with parallel execution.

This utility allows users to write simple per-individual functions while the framework handles batching/vectorization with parallel workers.

Parameters:

Name Type Description Default
fn Callable

Function that operates on one individual with signature (n_vars,) -> (n_out,). It must be picklable for multiprocessing.

required
n_workers int

Number of parallel workers. Use -1 for all CPU cores.

required

Returns:

Type Description
Callable

Function that operates on a population in parallel with signature (n, n_vars) -> (n, n_out).

Examples:

>>> def evaluate_one(x: np.ndarray) -> np.ndarray:
...     return np.array([x.sum(), x.prod()])
>>> evaluate = lift_parallel(evaluate_one, n_workers=4)
>>> pop_x = np.array([[1.0, 2.0], [3.0, 4.0]])
>>> evaluate(pop_x)
array([[ 3.,  2.],
       [ 7., 12.]])

select_parents

select_parents(
    pop: Population,
    n_parents: int,
    rng: Generator,
    rank: ndarray,
    crowding_distance: ndarray,
) -> np.ndarray

Select parents using binary tournament selection (vectorized).

Uses the crowded comparison operator: lower rank wins, ties broken by higher crowding distance.

Parameters:

Name Type Description Default
pop Population

Population used for its size.

required
n_parents int

Number of parents to select.

required
rng Generator

Random number generator for reproducibility.

required
rank ndarray

Pareto front ranks for all individuals. Shape is (n,).

required
crowding_distance ndarray

Crowding distances for all individuals. Shape is (n,).

required

Returns:

Type Description
ndarray

Array of shape (n_parents,) containing indices into pop.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.operators.selection import select_parents
>>> pop = Population(x=np.zeros((4, 2)), objectives=np.zeros((4, 2)))
>>> rng = np.random.default_rng(0)
>>> parents = select_parents(
...     pop,
...     n_parents=10,
...     rng=rng,
...     rank=np.array([0, 0, 1, 1]),
...     crowding_distance=np.array([1.0, 2.0, 1.0, 2.0]),
... )
>>> parents.shape
(10,)

create_offspring

create_offspring(
    pop: Population,
    n_offspring: int,
    crossover: Callable[[ndarray, ndarray], ndarray],
    mutate: Callable[[ndarray], ndarray],
    rng: Generator,
    rank: ndarray,
    crowding_distance: ndarray,
) -> np.ndarray

Create offspring via selection, crossover, and mutation.

Selects 2*n_offspring parents using binary tournament, crosses them in pairs, and applies mutation to all offspring.

Parameters:

Name Type Description Default
pop Population

Parent population.

required
n_offspring int

Number of offspring to create.

required
crossover Callable[[ndarray, ndarray], ndarray]

Crossover function with signature (n_vars,), (n_vars,) -> (n_vars,).

required
mutate Callable[[ndarray], ndarray]

Mutation function with signature (n_vars,) -> (n_vars,).

required
rng Generator

Random number generator for reproducibility.

required
rank ndarray

Pareto front ranks for all individuals. Shape is (n,).

required
crowding_distance ndarray

Crowding distances for all individuals. Shape is (n,).

required

Returns:

Type Description
ndarray

Array of shape (n_offspring, n_vars) containing unevaluated offspring decision variables.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.operators.selection import create_offspring
>>> pop = Population(x=np.arange(8.0).reshape(4, 2), objectives=np.zeros((4, 2)))
>>> rng = np.random.default_rng(0)
>>> crossover = lambda p1, p2: (p1 + p2) / 2
>>> mutate = lambda x: x.copy()
>>> offspring = create_offspring(
...     pop,
...     n_offspring=3,
...     crossover=crossover,
...     mutate=mutate,
...     rng=rng,
...     rank=np.array([0, 0, 1, 1]),
...     crowding_distance=np.array([1.0, 2.0, 1.0, 2.0]),
... )
>>> offspring.shape
(3, 2)

Selection strategies

crowded_tournament

crowded_tournament(tournament_size: int = 2)

Create a crowded tournament parent selector.

In crowded tournament selection, individuals are compared by: 1. Pareto rank (lower is better) 2. If ranks are equal, crowding distance (higher is better for diversity)

Parameters:

Name Type Description Default
tournament_size int

Number of individuals in each tournament.

2

Returns:

Type Description
callable

Parent selector that returns selected parent indices.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.selection.crowded import crowded_tournament
>>> pop = Population(x=np.zeros((4, 2)), objectives=np.zeros((4, 2)))
>>> rng = np.random.default_rng(0)
>>> rank = np.array([0, 0, 1, 1])
>>> cd = np.array([1.0, 2.0, 1.0, 2.0])
>>> selector = crowded_tournament(tournament_size=2)
>>> parents = selector(pop, 6, rng, rank=rank, crowding_distance=cd)
>>> parents.shape
(6,)

fitness_tournament

fitness_tournament(tournament_size: int = 2)

Create a fitness-based tournament parent selector for single-objective optimization.

Parameters:

Name Type Description Default
tournament_size int

Number of individuals competing in each tournament.

2

Returns:

Type Description
callable

Parent selector that returns selected parent indices.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.selection.tournament import fitness_tournament
>>> pop = Population(x=np.zeros((4, 2)), objectives=np.array([[3.0], [1.0], [2.0], [4.0]]))
>>> selector = fitness_tournament(tournament_size=2)
>>> parents = selector(pop, 5, np.random.default_rng(0))
>>> parents.shape
(5,)

roulette_wheel

roulette_wheel()

Create a roulette wheel (fitness-proportionate) parent selector.

Selection probability is inversely proportional to fitness because lower fitness is better (minimization). Uses max_fitness - fitness to convert minimization to maximization probabilities.

For fitness values f_i, the selection probability p_i is computed as: weights_i = max_fitness - f_i + ε p_i = weights_i / Σ(weights_j)

where ε is a small constant to ensure the worst individual has non-zero probability.

Returns:

Type Description
callable

Parent selector that performs fitness-proportionate selection.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.selection.roulette import roulette_wheel
>>> pop = Population(x=np.zeros((4, 2)), objectives=np.array([[4.0], [1.0], [2.0], [3.0]]))
>>> selector = roulette_wheel()
>>> parents = selector(pop, 5, np.random.default_rng(0))
>>> parents.shape
(5,)

Survival strategies

nsga2_survival

nsga2_survival()

Create NSGA-II survivor selector.

The NSGA-II survival strategy implements elitist selection by: 1. Computing Pareto ranks using non-dominated sorting 2. Filling survivors front-by-front in rank order 3. When a front only partially fits, selecting individuals with highest crowding distance (most isolated in objective space)

This preserves both convergence (keeping better fronts) and diversity (preferring isolated individuals within a front).

Returns:

Type Description
callable

Survivor selector that returns selected indices and rank/crowding state.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.survival.nsga2 import nsga2_survival
>>> obj = np.array([[1.0, 4.0], [2.0, 3.0], [3.0, 2.0], [4.0, 1.0]])
>>> pop = Population(x=np.zeros((4, 1)), objectives=obj)
>>> selector = nsga2_survival()
>>> indices, state = selector(pop, n_survivors=2)
>>> indices.shape
(2,)
>>> sorted(state)
['crowding_distance', 'rank']

truncation_survival

truncation_survival()

Create truncation survivor selector.

Truncation survival keeps the k best individuals by fitness value. Lower fitness is better (minimization).

Returns:

Type Description
callable

Survivor selector callable.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.survival.truncation import truncation_survival
>>> obj = np.array([[4.0], [2.0], [3.0], [1.0]])
>>> pop = Population(x=np.zeros((4, 1)), objectives=obj)
>>> indices, state = truncation_survival()(pop, n_survivors=2)
>>> indices
array([3, 1])
>>> state["fitness"].shape
(2,)

elitist_survival

elitist_survival(elite_count: int = 1)

Create elitist survivor selector.

Elitist survival always keeps the best elite_count individuals from the parent population, filling remaining slots with the best offspring.

Parameters:

Name Type Description Default
elite_count int

Number of elite parents to preserve.

1

Returns:

Type Description
callable

Survivor selector function.

Examples:

>>> import numpy as np
>>> from ctrl_freak.population import Population
>>> from ctrl_freak.survival.elitist import elitist_survival
>>> obj = np.array([[2.0], [1.0], [4.0], [0.5]])
>>> pop = Population(x=np.zeros((4, 1)), objectives=obj)
>>> indices, state = elitist_survival(elite_count=1)(pop, 2, parent_size=2)
>>> indices.shape
(2,)
>>> state["fitness"].shape
(2,)

Primitives

dominates

dominates(a: ndarray, b: ndarray) -> bool

Check if solution a Pareto-dominates solution b (minimization).

A solution a dominates b when it is no worse in every objective and strictly better in at least one objective.

Parameters:

Name Type Description Default
a ndarray

Objective values for solution a with shape (n_obj,).

required
b ndarray

Objective values for solution b with shape (n_obj,).

required

Returns:

Type Description
bool

True if a dominates b.

Examples:

>>> dominates(np.array([1.0, 2.0]), np.array([2.0, 3.0]))
True
>>> dominates(np.array([1.0, 3.0]), np.array([2.0, 2.0]))
False

dominates_matrix

dominates_matrix(objectives: ndarray) -> np.ndarray

Compute pairwise dominance for all individuals (vectorized).

Uses broadcasting to efficiently compute whether individual i dominates individual j for all pairs (i, j).

Parameters:

Name Type Description Default
objectives ndarray

Objective values for all individuals with shape (n, n_obj).

required

Returns:

Type Description
ndarray

Boolean array of shape (n, n) where result[i, j] is True iff individual i dominates individual j.

Notes

This implementation materializes an intermediate broadcast tensor with memory complexity O(N^2 * M) for N individuals and M objectives. The result is correct and vectorized; for very large N or M, prefer a chunked reduction.

Examples:

>>> objs = np.array([[1.0, 1.0], [2.0, 2.0], [1.0, 2.0]])
>>> dom = dominates_matrix(objs)
>>> bool(dom[0, 1])  # Does [1,1] dominate [2,2]?
True
>>> bool(dom[0, 2])  # Does [1,1] dominate [1,2]?
True

non_dominated_sort

non_dominated_sort(objectives: ndarray) -> np.ndarray

Assign each individual to a Pareto front using Deb's fast algorithm.

Implements the fast non-dominated sorting algorithm from NSGA-II. The time complexity is O(M * N^2) where M is the number of objectives and N is the population size.

Parameters:

Name Type Description Default
objectives ndarray

Objective values for all individuals with shape (n, n_obj).

required

Returns:

Type Description
ndarray

Integer array of shape (n,) where rank[i] is the front index for individual i. Rank 0 is the first Pareto front.

Examples:

>>> objs = np.array([[1.0, 1.0], [2.0, 2.0], [3.0, 3.0]])
>>> non_dominated_sort(objs)
array([0, 1, 2])

crowding_distance

crowding_distance(front_objectives: ndarray) -> np.ndarray

Compute crowding distance for individuals in a single Pareto front.

Crowding distance measures how isolated a solution is in objective space. Higher values indicate more isolated solutions (preferred for diversity).

Boundary solutions (with min/max values for any objective) receive infinite distance. Interior solutions receive the sum of normalized neighbor distances across all objectives.

Parameters:

Name Type Description Default
front_objectives ndarray

Objective values for individuals in one front only, with shape (n_front, n_obj).

required

Returns:

Type Description
ndarray

Array of shape (n_front,) containing crowding distances. Higher values indicate more isolated solutions.

Examples:

>>> objs = np.array([[1.0, 4.0], [2.0, 3.0], [3.0, 2.0], [4.0, 1.0]])
>>> cd = crowding_distance(objs)
>>> bool(np.isinf(cd[0]) and np.isinf(cd[-1]))  # Boundary points
True

Registry

SelectionRegistry

Registry for parent selection strategies.

This class provides a class-level registry for parent selection strategy factories. Strategies are registered by name and can be retrieved with custom configuration parameters.

The registry stores factory functions that accept keyword arguments and return ParentSelector callables. This enables flexible configuration at retrieval time.

Attributes:

Name Type Description
_registry dict[str, Callable[..., ParentSelector]]

Mapping from strategy names to factory functions.

Examples:

A strategy factory returns a callable that satisfies ParentSelector::

def tournament_factory(size=2):
    def selector(pop, n_parents, rng, **kwargs):
        ...
    return selector

SelectionRegistry.register("tournament", tournament_factory)
selector = SelectionRegistry.get("tournament", size=5)

register classmethod

register(
    name: str, factory: Callable[..., ParentSelector]
) -> None

Register a parent selection strategy factory.

The factory is a callable that accepts keyword arguments and returns a ParentSelector. This enables strategies to be configured at retrieval time with custom parameters.

Parameters:

Name Type Description Default
name str

Unique name for the strategy. Existing names are overwritten.

required
factory Callable[..., ParentSelector]

Callable that returns a parent selector.

required

Examples:

>>> from ctrl_freak.registry import SelectionRegistry
>>> SelectionRegistry.register(
...     "__doc_random__",
...     lambda: lambda pop, n_parents, rng, **kw: rng.choice(len(pop), n_parents),
... )
>>> "__doc_random__" in SelectionRegistry.list()
True

get classmethod

get(name: str, **kwargs) -> ParentSelector

Get a configured parent selector by name.

Retrieves the factory for the given strategy name and calls it with the provided keyword arguments to create a configured selector.

Parameters:

Name Type Description Default
name str

Name of the registered strategy.

required
**kwargs

Configuration parameters passed to the factory function.

{}

Returns:

Type Description
ParentSelector

A configured parent selector callable.

Raises:

Type Description
KeyError

If the strategy name is not registered.

Examples:

>>> from ctrl_freak.registry import SelectionRegistry
>>> SelectionRegistry.register("__doc_empty__", lambda: lambda pop, n, rng, **kw: [])
>>> selector = SelectionRegistry.get("__doc_empty__")
>>> callable(selector)
True

list classmethod

list() -> list[str]

Return list of registered strategy names.

Returns:

Type Description
list[str]

Sorted list of all registered parent selection strategy names.

Examples:

>>> from ctrl_freak.registry import SelectionRegistry
>>> isinstance(SelectionRegistry.list(), list)
True

SurvivalRegistry

Registry for survivor selection strategies.

This class provides a class-level registry for survivor selection strategy factories. Strategies are registered by name and can be retrieved with custom configuration parameters.

The registry stores factory functions that accept keyword arguments and return SurvivorSelector callables. This enables flexible configuration at retrieval time.

Attributes:

Name Type Description
_registry dict[str, Callable[..., SurvivorSelector]]

Mapping from strategy names to factory functions.

Examples:

A strategy factory returns a callable that satisfies SurvivorSelector::

def nsga2_factory(preserve_diversity=True):
    def selector(pop, n_survivors, **kwargs):
        ...
    return selector

SurvivalRegistry.register("nsga2", nsga2_factory)
selector = SurvivalRegistry.get("nsga2", preserve_diversity=False)

register classmethod

register(
    name: str, factory: Callable[..., SurvivorSelector]
) -> None

Register a survivor selection strategy factory.

The factory is a callable that accepts keyword arguments and returns a SurvivorSelector. This enables strategies to be configured at retrieval time with custom parameters.

Parameters:

Name Type Description Default
name str

Unique name for the strategy. Existing names are overwritten.

required
factory Callable[..., SurvivorSelector]

Callable that returns a survivor selector.

required

Examples:

>>> import numpy as np
>>> from ctrl_freak.registry import SurvivalRegistry
>>> SurvivalRegistry.register(
...     "__doc_truncation__",
...     lambda: lambda pop, n, **kw: (np.arange(n), {}),
... )
>>> "__doc_truncation__" in SurvivalRegistry.list()
True

get classmethod

get(name: str, **kwargs) -> SurvivorSelector

Get a configured survivor selector by name.

Retrieves the factory for the given strategy name and calls it with the provided keyword arguments to create a configured selector.

Parameters:

Name Type Description Default
name str

Name of the registered strategy.

required
**kwargs

Configuration parameters passed to the factory function.

{}

Returns:

Type Description
SurvivorSelector

A configured survivor selector callable.

Raises:

Type Description
KeyError

If the strategy name is not registered.

Examples:

>>> from ctrl_freak.registry import SurvivalRegistry
>>> SurvivalRegistry.register("__doc_survivor__", lambda: lambda pop, n, **kw: ([], {}))
>>> selector = SurvivalRegistry.get("__doc_survivor__")
>>> callable(selector)
True

list classmethod

list() -> list[str]

Return list of registered strategy names.

Returns:

Type Description
list[str]

Sorted list of all registered survivor selection strategy names.

Examples:

>>> from ctrl_freak.registry import SurvivalRegistry
>>> isinstance(SurvivalRegistry.list(), list)
True

list_selections

list_selections() -> list[str]

List all registered parent selection strategies.

Convenience function that returns SelectionRegistry.list().

Returns:

Type Description
list[str]

Sorted list of all registered parent selection strategy names.

Examples:

>>> from ctrl_freak.registry import list_selections
>>> isinstance(list_selections(), list)
True

list_survivals

list_survivals() -> list[str]

List all registered survivor selection strategies.

Convenience function that returns SurvivalRegistry.list().

Returns:

Type Description
list[str]

Sorted list of all registered survivor selection strategy names.

Examples:

>>> from ctrl_freak.registry import list_survivals
>>> isinstance(list_survivals(), list)
True

Protocols

ParentSelector

Bases: Protocol

Protocol for parent selection strategies.

Parent selectors choose which individuals from a population will be used as parents for creating offspring. Different strategies (tournament, roulette wheel, crowded selection) implement this protocol.

The selector is called with the population, number of parents to select, a random number generator, and optional keyword arguments containing algorithm-specific data (e.g., rank, crowding_distance for NSGA-II).

Parameters:

Name Type Description Default
pop Population

The current population to select parents from.

required
n_parents int

Number of parent indices to return. The same individual may be selected multiple times depending on the strategy.

required
rng Generator

Random number generator for reproducible stochastic selection.

required
**kwargs ndarray

Algorithm-specific state data. NSGA-II selectors typically receive rank and crowding_distance arrays. Single-objective selectors may receive fitness values.

required

Returns:

Type Description
ndarray

Indices into the population. Shape is (n_parents,) with values in [0, len(pop)).

Example implementations:
  • Tournament selection: Compare k random individuals, select best
  • Roulette wheel: Probability proportional to fitness
  • Crowded tournament (NSGA-II): Compare by rank, then crowding distance
  • Random selection: Uniform random choice

Examples:

A selector implementation is a callable with the protocol signature::

def tournament_selector(pop, n_parents, rng, **kwargs):
    fitness = kwargs["fitness"]
    indices = []
    for _ in range(n_parents):
        candidates = rng.choice(len(pop), size=2, replace=False)
        winner = candidates[np.argmin(fitness[candidates])]
        indices.append(winner)
    return np.array(indices)

SurvivorSelector

Bases: Protocol

Protocol for survivor selection strategies.

Survivor selectors determine which individuals survive to the next generation from a combined parent+offspring population. Different strategies (NSGA-II, truncation, elitist) implement this protocol.

The selector is called with the population, number of survivors to select, and optional keyword arguments containing algorithm-specific data.

Parameters:

Name Type Description Default
pop Population

Combined population, typically parents plus offspring.

required
n_survivors int

Number of individuals to select for the next generation.

required
**kwargs Any

Algorithm-specific input data. Values may include arrays, pre-computed metrics, or non-array controls such as the integer parent_size.

required

Returns:

Type Description
tuple[ndarray, dict[str, ndarray]]

Selected survivor indices and algorithm-specific state for the next generation.

Example implementations:
  • NSGA-II: Non-dominated sorting + crowding distance truncation
  • Truncation: Keep top n_survivors by fitness
  • Elitist: Always keep best individual, random for rest
  • Age-based: Prefer younger individuals

Examples:

A selector implementation returns survivor indices and state::

def truncation_selector(pop, n_survivors, **kwargs):
    fitness = pop.objectives[:, 0]
    survivor_indices = np.argsort(fitness)[:n_survivors]
    return survivor_indices, {"fitness": fitness[survivor_indices]}