Extending the Framework with Custom Components

You can provide every major component of an optimisation algorithm as a plain Python function, wrapped in dedicated *FromLambda classes. For operators and selection methods there are also factories that let you register your function and then retrieve it by name.

Throughout this guide the following type aliases are used for clarity:

  • MatrixLike – a 2‑D NumPy array (n_individuals × n_vars).

  • VectorLike – a 1‑D NumPy array (fitness values).

  • RNGLike – a NumPy Generator or a seed.

All examples assume maximisation (higher fitness is better); if your problem minimises, set mode="min" in the objective function.

Objective Function

Wrap an evaluation function with ObjectiveFromLambda.

def my_obj(solution: Any, **kwargs) -> float | np.ndarray:
    ...
    return fitness_value
  • solution – the decoded individual (any Python object).

  • Must return a single numeric value.

  • Any extra keyword arguments given to the constructor are forwarded as **kwargs.

from metaheuristic_designer import ObjectiveFromLambda

def sphere(vec, offset=0):
    return -np.sum((vec - offset) ** 2)   # maximise negative squared distance

objfunc = ObjectiveFromLambda(sphere, dimension=3, offset=3.0, mode="max")

Constraint Handler

Use ConstraintHandlerFromLambda. At least one of the two possible callables must be provided.

def repair_fn(solution: Any) -> Any:
    """Return a repaired copy of the solution."""
    ...

def penalty_fn(solution: Any) -> float:
    """Return a penalty that will be subtracted from the fitness."""
    ...
from metaheuristic_designer import ConstraintHandlerFromLambda

def clip_to_bounds(x, low=-5.0, high=5.0):
    return np.clip(x, low, high)

ch = ConstraintHandlerFromLambda(repair_solution_fn=clip_to_bounds, low=-5, high=5)

Initializer

Wrap a generator function with InitializerFromLambda.

def my_gen(random_state: RNGLike, **kwargs) -> np.ndarray:
    """Return a single new individual (genotype vector)."""
    ...
  • The function is called once per individual; it receives a random state and must return a 1‑D array.

from metaheuristic_designer import InitializerFromLambda

def uniform_gen(random_state, low=0.0, high=1.0):
    return random_state.uniform(low, high, size=5)

init = InitializerFromLambda(uniform_gen, dimension=5, pop_size=100, low=-10, high=10, size=5)

Encoding

Wrap an encode/decode pair with EncodingFromLambda.

def my_encode(solutions: Iterable) -> MatrixLike:
    """Encode a list of solutions into a genotype matrix (2-D array)."""
    ...

def my_decode(population_matrix: MatrixLike) -> Iterable:
    """Decode the whole genotype matrix into a list/array of solutions."""
    ...
from metaheuristic_designer import EncodingFromLambda

def to_ints(real_vec):
    return np.floor(real_vec).astype(int)

def to_reals(int_matrix):
    return int_matrix.astype(float)

enc = EncodingFromLambda(encode_fn=to_ints, decode_fn=to_reals)

Operators

There are two ways to write a custom operator, depending on the level of control you need.

Population‑level (advanced)

If you need to access or modify the whole Population object, provide a function that receives and returns a Population. Make a copy at the beginning; never mutate the original.

def my_pop_op(population: Population, initializer: Initializer,
              random_state: RNGLike, **kwargs) -> Population:
    pop_copy = copy(population)   # or population.__copy__()
    # … modify pop_copy …
    return pop_copy

Register it without a wrapper:

from metaheuristic_designer.operators import add_operator_entry

def duplicate_best(population, initializer, random_state):
    pop_copy = copy(population)
    best_gen = pop_copy.genotype_matrix[pop_copy.best_idx]
    pop_copy.genotype_matrix[:] = best_gen   # all individuals become the best
    return pop_copy

add_operator_entry(duplicate_best, "dup_best", "custom")
op = create_operator("custom.dup_best")

Parent Selection

The factory create_parent_selection expects a function that works on fitness arrays. If you need the whole population, instantiate ParentSelectionFromLambda directly.

Factory pathway (fitness‑level)

@ParentSelectionDef
def my_parent_select(fitness: VectorLike, amount: int,
                     random_state: RNGLike, **kwargs) -> np.ndarray:
    """Return indices of selected individuals."""
    ...
  • fitness – the current fitness values.

  • amount – how many individuals to select.

  • Must return a 1‑D integer array (no duplicates).

For it to be accepted into the registry, it must be passed to the ParentSelectionDef wrapper since the ParentSelection class works directly with metaheuristic_designer.population.Population objects. This can be easily done by using ParentSelectionDef as a decorator.

from metaheuristic_designer.parent_selection_methods import add_parent_selection_entry, ParentSelectionDef
from metaheuristic_designer import create_parent_selection

@ParentSelectionDef
def pick_top_k(fitness, amount, random_state, **kwargs):
    # Maximisation: higher fitness is better → use argpartition for top k
    top_idx = np.argpartition(-fitness, amount - 1)[:amount]
    return top_idx

add_parent_selection_entry(pick_top_k, "top_k")
sel = create_parent_selection("top_k", amount=20)

Direct pathway (Population‑level)

from metaheuristic_designer import ParentSelectionFromLambda

def pop_level_select(population: Population, amount: int,
                     random_state: RNGLike, **kwargs) -> np.ndarray:
    # Access population.genotype_matrix, population.fitness, etc.
    fitness = population.fitness
    top_idx = np.argpartition(-fitness, amount - 1)[:amount]
    return top_idx

sel = ParentSelectionFromLambda(pop_level_select, amount=20)

Survivor Selection

Similarly, the factory create_survivor_selection works with fitness‑level functions, while direct instantiation of SurvivorSelectionFromLambda gives access to the Population objects.

Factory pathway (fitness‑level)

@SurvivorSelectionDef
def my_survivor_select(parent_fitness: VectorLike,
                       offspring_fitness: VectorLike,
                       random_state: RNGLike, **kwargs) -> np.ndarray:
    """Return indices into the concatenated [parents, offspring]."""
    ...
  • parent_fitness – fitness of the parent population.

  • offspring_fitness – fitness of the offspring.

  • The returned indices refer to the array obtained by joining parents and offspring.

For it to be accepted into the registry, it must be passed to the SurvivorSelectionDef wrapper since the SurvivorSelection class works directly with metaheuristic_designer.population.Population objects. This can be easily done by using SurvivorSelectionDef as a decorator.

from metaheuristic_designer.survivor_selection_methods import add_survivor_selection_entry
from metaheuristic_designer import create_survivor_selection

@SurvivorSelectionDef
def keep_all_offspring(parent_fit, offspring_fit, random_state, **kwargs):
    n_parents = len(parent_fit)
    n_offspring = len(offspring_fit)
    return np.arange(n_parents, n_parents + n_offspring)

add_survivor_selection_entry(keep_all_offspring, "all_offspring")
ss = create_survivor_selection("all_offspring")

Direct pathway (Population‑level)

from metaheuristic_designer import SurvivorSelectionFromLambda
from metaheuristic_designer.population import Population

def pop_level_survivor(parents: Population, offspring: Population,
                       random_state: RNGLike, **kwargs) -> np.ndarray:
    # Compute fitness arrays, decide survivors
    combined_fit = np.concatenate([parents.fitness, offspring.fitness])
    n = len(parents)
    return np.argpartition(-combined_fit, n - 1)[:n]

ss = SurvivorSelectionFromLambda(pop_level_survivor)

Utility Decorators for Row‑wise Operations

When writing custom operators (or other matrix‑level functions) it is often convenient to think in terms of “apply this function to each individual’s row”. Two decorators from metaheuristic_designer.utils make this trivial:

  • per_individual – wraps a function that operates on a single row (1‑D array) so that it can be called on a 2‑D matrix and returns a 2‑D matrix of the same shape.

  • per_individual_list – same idea, but works on a list of objects (e.g. decoded solutions), returning a list.

from metaheuristic_designer.utils import per_individual

@per_individual
def small_noise_vector(row, scale=0.1, random_state=None):
    rng = np.random.default_rng(random_state)
    return row + rng.normal(0, scale, size=row.shape)

# Now small_noise_vector can be used as a matrix‑level operator function
from metaheuristic_designer.operators import OperatorFnDef, add_operator_entry

add_operator_entry(OperatorFnDef(small_noise_vector), "tiny_noise", "custom")
op = create_operator("custom.tiny_noise", scale=0.05, random_state=42)