Source code for pymoo.algorithms.moo.nsde

"""NSDE - NSGA-II with differential evolution operators."""

import numpy as np

from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.core.infill import InfillCriterion
from pymoo.core.population import Population
from pymoo.operators.crossover.binx import mut_binomial
from pymoo.operators.crossover.dex import DE_REPAIRS
from pymoo.operators.crossover.expx import mut_exp
from pymoo.operators.mutation.nom import NoMutation
from pymoo.operators.sampling.lhs import LHS
from pymoo.operators.selection.des import DES
from pymoo.operators.survival.rank_and_crowding import RankAndCrowding


# =========================================================================================================
# VariantDE — DE mating operator used by NSDE-family algorithms
# =========================================================================================================


class VariantDE(InfillCriterion):
    def __init__(
        self,
        variant="DE/rand/1/bin",
        CR=0.7,
        F=None,
        gamma=1e-4,
        de_repair="bounce-back",
        mutation=None,
        **kwargs,
    ):

        super().__init__(eliminate_duplicates=None, **kwargs)

        _, selection_variant, n_diff, crossover_variant = variant.split("/")
        n_diffs = int(n_diff)
        if "-to-" in variant:
            n_diffs += 1

        self.selection = DES(selection_variant)
        # total parents: 1 target + 1 mutation base + 2*(n_diffs-1) additional diff vectors
        # = 2 + 2*n_diffs (matches PR's DEX.n_parents = 2 + 2*n_diffs)
        self.n_total_parents = 2 + 2 * n_diffs
        self.n_diffs = n_diffs
        self.crossover_variant = crossover_variant
        self.CR = CR
        self.gamma = gamma

        if F is None:
            F = (0.0, 1.0)
        self.F = F

        if callable(de_repair):
            self.de_repair = de_repair
        else:
            if de_repair not in DE_REPAIRS:
                raise KeyError(
                    f"de_repair must be callable or one of {list(DE_REPAIRS.keys())}"
                )
            self.de_repair = DE_REPAIRS[de_repair]

        self.mutation = mutation if mutation is not None else NoMutation()

    def _do(self, problem, pop, n_offsprings, random_state=None, **kwargs):
        n_var = problem.n_var

        # Select parents: shape [n_offsprings, n_total_parents]
        P = self.selection.do(
            problem,
            pop,
            n_offsprings,
            self.n_total_parents,
            to_pop=False,
            random_state=random_state,
        )

        X = pop.get("X")
        Xr = X[P]  # [n_offsprings, n_total_parents, n_var]

        # Mutation base is column 1 (column 0 is the target for crossover)
        V = Xr[:, 1].copy()

        # Difference pairs start at column 2: (col2,col3), (col4,col5), ...
        for k in range(self.n_diffs):
            ai, bi = 2 + 2 * k, 3 + 2 * k
            F = self._sample_F(n_offsprings, random_state)
            if self.gamma is not None:
                F_mat = F[:, None] * (
                    1 + self.gamma * (random_state.random((n_offsprings, n_var)) - 0.5)
                )
                V = V + F_mat * (Xr[:, ai] - Xr[:, bi])
            else:
                V = V + F[:, None] * (Xr[:, ai] - Xr[:, bi])

        # Repair donor vector if bounds are violated
        if problem.has_bounds():
            V = self.de_repair(
                V, Xr[:, 1], *problem.bounds(), random_state=random_state
            )

        # Crossover: trial vector from target (col 0) and donor V
        Xi = Xr[:, 0]
        if self.crossover_variant == "bin":
            M = mut_binomial(
                n_offsprings,
                n_var,
                self.CR,
                at_least_once=True,
                random_state=random_state,
            )
        elif self.crossover_variant == "exp":
            M = mut_exp(
                n_offsprings,
                n_var,
                self.CR,
                at_least_once=True,
                random_state=random_state,
            )
        else:
            raise ValueError(f"Unknown crossover variant: {self.crossover_variant}")

        trial = Xi.copy()
        trial[M] = V[M]

        off = Population.new("X", trial)

        # Optional posterior mutation
        off = self.mutation(problem, off, random_state=random_state)

        return off

    def _sample_F(self, n, random_state):
        if hasattr(self.F, "__iter__"):
            lo, hi = self.F[0], self.F[1]
            return lo + random_state.random(n) * (hi - lo)
        return np.full(n, self.F)


# =========================================================================================================
# NSDE
# =========================================================================================================


[docs] class NSDE(NSGA2): def __init__( self, pop_size=100, sampling=None, variant="DE/rand/1/bin", CR=0.7, F=None, gamma=1e-4, de_repair="bounce-back", survival=None, **kwargs, ): """NSDE combines NSGA-II sorting and survival with DE mutation and crossover. For many-objective problems, try NSDE-R, GDE3-MNN, or GDE3-2NN. For bi-objective problems, survival=RankAndCrowding(crowding_func='pcd') is effective. Args: pop_size: Population size. Defaults to 100. sampling: Sampling strategy. Defaults to LHS(). variant: DE strategy string: "DE/selection/n/crossover". selection: 'rand', 'best', 'current-to-best', 'current-to-rand', 'ranked'. crossover: 'bin' or 'exp'. Defaults to 'DE/rand/1/bin'. CR: Crossover rate in [0, 1]. Defaults to 0.7. F: Scale factor(s) in (0, 2]. Defaults to randomized in (0, 1). gamma: Jitter deviation. Defaults to 1e-4. de_repair: Repair for DE donor vectors: 'bounce-back', 'midway', 'rand-init', 'to-bounds'. survival: Survival strategy. Defaults to RankAndCrowding(). **kwargs: Additional keyword arguments passed to parent NSGA2. """ if sampling is None: sampling = LHS() if survival is None: survival = RankAndCrowding() mating = VariantDE( variant=variant, CR=CR, F=F, gamma=gamma, de_repair=de_repair ) super().__init__( pop_size=pop_size, sampling=sampling, mating=mating, survival=survival, eliminate_duplicates=None, n_offsprings=pop_size, **kwargs, )