Skip to content

Constraint Handling

Evolutionary algorithms are unconstrained by default. This page shows how to add feasibility to a run: a penalty on evaluate, and Deb constraint-dominance on NSGA-II. The walkthrough follows Coello Coello's survey of penalty methods. A complete script is the constraints example.

Penalty function

A penalty gives an infeasible individual a worse fitness, constant or growing with distance to the valid region. For a scalar \(x\) constrained to \(3 < x < 7\), the assigned fitness is \(g(x)\) inside the interval and \(h(x)\) outside. \(h(x)\) may be a constant \(\Delta\), \(\Delta\) plus Euclidean distance to a feasible point, or \(\Delta\) plus a quadratic distance.

DeltaPenalty wraps any evaluation function. The first argument is a feasibility predicate. The second is \(\Delta\). The optional third is a distance to the valid region:

from math import sin
from deap_er import Toolbox, tools

def evaluate(individual):
    x = individual[0]
    return ((x - 5) ** 2 * sin(x) * (x / 3),)

def feasible(individual):
    return 3 < individual[0] < 7

def distance(individual):
    return (individual[0] - 5.0) ** 2

toolbox = Toolbox()
toolbox.register("evaluate", evaluate)
toolbox.decorate("evaluate", tools.DeltaPenalty(feasible, 7.0, distance))

Valid individuals keep \(g(x)\). Invalid ones receive \(\Delta\) adjusted by the distance term (the sign follows fitness.weights).

ClosestValidPenalty is the other decorator: an invalid individual is scored as the closest feasible individual plus \(\alpha\) times a distance. See the MO-CMA example and the utilities reference.

Constraint-dominance

Deb's rule compares feasibility first, then violation, then ordinary Pareto dominance. constraint_dominates is that comparison. sel_nsga_2 accepts optional feasible= and violation= callables and ranks the pool with that order:

def violation(individual):
    return max(0.0, individual[0] ** 2 + individual[1] ** 2 - 1.0)

toolbox.register(
    "select",
    tools.sel_nsga_2,
    feasible=lambda ind: violation(ind) == 0.0,
    violation=violation,
)

Use this when the run is already multi-objective and you want infeasible points to lose to feasible ones before crowding. Pair it with a penalty if evaluate must still return a number for an invalid genome, or return the raw objectives and let selection enforce the constraint.

See the Operators reference for the full feasible / violation contract.