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
|
callback
|
Callable[[NSGA2Result, int], bool] | None
|
Optional callback called before each generation. Return |
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 |
1
|
evaluate_batch
|
Callable[[ndarray], ndarray] | None
|
Optional whole-population objective. When provided, it receives the
entire |
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
|
callback
|
Callable[[GAResult, int], bool] | None
|
Optional callback called before each generation. Return |
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 |
1
|
evaluate_batch
|
Callable[[ndarray], ndarray] | None
|
Optional whole-population objective. When provided, it receives the
entire |
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
¶
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
¶
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
¶
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 |
objectives |
ndarray | None
|
Objective values. Shape is |
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
¶
Return the number of individuals in the population.
Returns:
| Type | Description |
|---|---|
int
|
Number of individuals. |
n_vars
property
¶
Return the number of decision variables per individual.
Returns:
| Type | Description |
|---|---|
int
|
Number of decision variables. |
n_obj
property
¶
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
¶
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 |
None
|
Returns:
| Type | Description |
|---|---|
Callable
|
Callable with signature |
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:
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 |
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 |
None
|
Returns:
| Type | Description |
|---|---|
Callable
|
Callable with signature |
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:
lift ¶
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
|
required |
Returns:
| Type | Description |
|---|---|
Callable
|
Function that operates on a population with signature
|
Examples:
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
|
required |
n_workers
|
int
|
Number of parallel workers. Use |
required |
Returns:
| Type | Description |
|---|---|
Callable
|
Function that operates on a population in parallel with signature
|
Examples:
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 |
required |
crowding_distance
|
ndarray
|
Crowding distances for all individuals. Shape is |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of shape |
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 |
required |
mutate
|
Callable[[ndarray], ndarray]
|
Mutation function with signature |
required |
rng
|
Generator
|
Random number generator for reproducibility. |
required |
rank
|
ndarray
|
Pareto front ranks for all individuals. Shape is |
required |
crowding_distance
|
ndarray
|
Crowding distances for all individuals. Shape is |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of shape |
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 |
required |
b
|
ndarray
|
Objective values for solution |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Examples:
dominates_matrix ¶
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 |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Boolean array of shape |
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:
non_dominated_sort ¶
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 |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Integer array of shape |
Examples:
crowding_distance ¶
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
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of shape |
Examples:
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 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:
get
classmethod
¶
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:
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 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:
get
classmethod
¶
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:
list_selections ¶
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:
list_survivals ¶
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:
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
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Indices into the population. Shape is |
Example implementations:
|
|
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 |
required |
Returns:
| Type | Description |
|---|---|
tuple[ndarray, dict[str, ndarray]]
|
Selected survivor indices and algorithm-specific state for the next generation. |
Example implementations:
|
|
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]}