ConstraintHandler#

Layer 3: Extend Components

The basic way to define constraints (InequalityConstraint/EqualityConstraint), how to specify Problem’s handler argument, and how to choose among the three built-in handlers are covered in Constrained Optimization. This page focuses on writing a custom ConstraintHandler, building on that foundation.

ConstraintHandler’s role#

ConstraintHandler exposes its handling of constraints as a set of swappable lifecycle hooks.

Ask            -> [repair(x, constraints, lb, ub)]
               -> evaluate f, g
               -> [compute_cv(constraints, x, g)]        -> cv
               -> [augment_objective(f, constraints, x, g)] -> f'
Tell           -> Comparator(f', cv) with eps_cv = feasibility_threshold
Generation end -> [on_generation_end(gen, population)]

Only compute_cv is abstract; every other hook has a default implementation. A custom ConstraintHandler only needs to override the hooks it needs.

repair(x, constraints, lb, ub, **kwargs): Called after crossover and mutation, before evaluation. The default is np.clip(x, lb, ub) — simply clipping to the bounds.

compute_cv(constraints, x, g) (abstract): Aggregates the set of constraints into a single cv value.

augment_objective(f, constraints, x, g): Transforms the objective value using constraint information. The default is the identity function (no transformation) — this is where you’d override to implement a penalty-function method or the augmented Lagrangian method. None of the three built-in handlers override this hook, so it’s currently left open as a future extension point.

feasibility_threshold (property): Defaults to 1e-6. This value is used as Comparator’s eps_cv, and is synchronized every generation during an Optimizer run.

on_generation_end(gen, population): Called at the end of a generation. The default is a no-op; used by handlers with internal state that updates an ε value per generation, such as EpsilonConstraintHandler.

Hooks overridden by the built-in handlers#

Class

Hooks overridden

StaticToleranceHandler

compute_cv / feasibility_threshold

EpsilonConstraintHandler

compute_cv / feasibility_threshold / on_generation_end

GradientRepairHandler

repair / compute_cv

EpsilonConstraintHandler [1] updates ε per generation via a function schedule(gen) -> float. Built-in schedule-generating functions linear_epsilon_schedule(eps0, n_gen)/exponential_epsilon_schedule(eps0, decay) are also exposed.

GradientRepairHandler [2] overrides repair() with a simultaneous Moore-Penrose pseudoinverse update over the currently-violated constraints (equality and inequality alike). Constraints whose gradient() returns None fall back to a forward-difference numerical approximation rather than being skipped.

Extension points on InequalityConstraint/EqualityConstraint#

InequalityConstraint itself also has two extension points.

gradient(x): Returns None by default. Override it to return an analytical gradient vector; otherwise GradientRepairHandler falls back to a numerical approximation.

violation_from_value(g): Defines the conversion from the raw constraint value g(x) to a violation amount. The default is max(0, g - threshold). EqualityConstraint overrides only this method, defining its own conversion, max(0, |h(x)| - tolerance).

Implementing a custom ConstraintHandler#

If you need a custom constraint-handling strategy, subclass ConstraintHandler and implement only compute_cv(). The following example is a penalty-function method that adds the constraint violation to the objective value.

from saealib import ConstraintHandler


class PenaltyHandler(ConstraintHandler):
    """Adds the violation amount to the objective value as a penalty."""

    def __init__(self, penalty_coeff: float = 1e3):
        self.penalty_coeff = penalty_coeff

    def compute_cv(self, constraints, x, g):
        return sum(max(0.0, gi) for gi in g)

    def augment_objective(self, f, constraints, x, g):
        cv = self.compute_cv(constraints, x, g)
        return f + self.penalty_coeff * cv

While overriding augment_objective to add the penalty to the objective value, you can also implement compute_cv to always return 0 as the constraint violation, giving a configuration that handles infeasibility purely through the penalty-function method (treating every solution as feasible).

References#