Process Engineer & Carbon Capture Researcher

NSGA-II Optimization Reference

A personal technical reference on multi-objective optimization, compiled from hands-on exposure with pymoo.

For educational use. This reference is compiled from hands-on exposure with pymoo's NSGA-II implementation. It is shared as an open resource — outputs and recommendations should always be validated against your own problem and reviewed with engineering judgement before use in any design or decision-making context. The author accepts no liability for results applied beyond this intended educational purpose.

pymoo version: 0.6.1.6
Module: pymoo.algorithms.moo.nsga2
Reference: Deb et al. (2002) — “A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II”


Table of Contents

  1. Overview
  2. Algorithm Summary
  3. Full Signature
  4. Core Parameters (kwargs)
  5. Operator Sub-Parameters
  6. Inherited kwargs (Algorithm Base Class)
  7. minimize() Function Parameters
  8. DefaultMultiObjectiveTermination
  9. Result Object — Complete Field Reference
  10. Verbose Output — Column Meanings
  11. Sampling Options Reference
  12. Crossover Options Reference
  13. Mutation Options Reference
  14. Performance Indicators
  15. Parallelization
  16. When NOT to Use NSGA-II
  17. Full Configuration Examples

Overview

NSGA-II (Non-dominated Sorting Genetic Algorithm II) is a fast, elitist, multi-objective evolutionary algorithm. It is the go-to baseline algorithm for solving problems with 2-3 objectives in pymoo. Its key features are:

  • Non-dominated sorting to rank solutions into Pareto fronts
  • Crowding distance for diversity preservation within a front
  • Binary tournament selection for selection pressure
  • Elitism — the best individuals always survive to the next generation

Algorithm Summary

Phase Mechanism
Initialization Random sampling (or user-supplied population / array)
Parent Selection Binary tournament (rank then crowding distance)
Recombination Simulated Binary Crossover (SBX)
Mutation Polynomial Mutation (PM)
Survival Rank-and-Crowding (non-dominated sort + crowding distance)
Termination n_gen, n_eval, time, tolerance-based, or custom

Full Signature

from pymoo.algorithms.moo.nsga2 import NSGA2

NSGA2(
    pop_size=100,
    sampling=FloatRandomSampling(),
    selection=TournamentSelection(func_comp=binary_tournament),
    crossover=SBX(prob=0.9, eta=15),
    mutation=PM(prob=None, eta=20),
    survival=RankAndCrowding(),
    output=MultiObjectiveOutput(),
    eliminate_duplicates=True,
    n_offsprings=None,
    repair=None,
    mating=None,
    # --- Inherited from Algorithm base ---
    termination=None,
    seed=None,
    verbose=False,
    callback=None,
    save_history=False,
    return_least_infeasible=False,
    display=None,
    evaluator=None,
    archive=None,
    **kwargs
)

Core Parameters (kwargs)

pop_size

   
Type int
Default 100

The number of individuals maintained in the population each generation. This is one of the most impactful hyperparameters:

  • Larger values (e.g., 200-500) improve diversity and exploration but significantly increase runtime per generation.
  • Smaller values (e.g., 20-50) run faster but risk premature convergence, especially in complex or high-dimensional search spaces.
  • A common rule of thumb is to set pop_size to at least 10x the number of decision variables.
  • For problems with many constraints, a larger population helps maintain enough feasible individuals.
algorithm = NSGA2(pop_size=200)

sampling

   
Type Sampling object, Population object, or numpy.ndarray
Default FloatRandomSampling()

Defines how the initial population is generated. Three input formats are accepted:

  1. A Sampling implementation - a class that generates random initial solutions within the problem’s bounds.
  2. A Population object - pass pre-built individuals, optionally already evaluated (set F values in this case).
  3. A 2D numpy.ndarray - shape (n_individuals, n_var) with variable values, used for warm-starting the search from a known region.
import numpy as np
from pymoo.operators.sampling.rnd import FloatRandomSampling, BinaryRandomSampling
from pymoo.operators.sampling.lhs import LHS

# Option 1: Random float sampling (default)
NSGA2(sampling=FloatRandomSampling())

# Option 2: Latin Hypercube Sampling for better initial coverage
NSGA2(sampling=LHS())

# Option 3: Binary sampling (for binary variable problems)
NSGA2(sampling=BinaryRandomSampling())

# Option 4: Warm-start with a custom numpy array
X_init = np.random.rand(100, 10)  # 100 individuals, 10 variables
NSGA2(sampling=X_init)

selection

   
Type Selection object
Default TournamentSelection(func_comp=binary_tournament)

Determines how parent individuals are chosen from the current population for mating. The default binary tournament selection compares two randomly chosen individuals and selects the better one based on:

  1. Pareto rank (lower is better - closer to the Pareto front)
  2. Crowding distance (higher is better - less crowded regions preferred)
from pymoo.operators.selection.tournament import TournamentSelection
from pymoo.operators.selection.rnd import RandomSelection
from pymoo.algorithms.moo.nsga2 import binary_tournament

# Default: binary tournament
NSGA2(selection=TournamentSelection(func_comp=binary_tournament))

# Alternative: purely random selection (removes selection pressure)
NSGA2(selection=RandomSelection())

crossover

   
Type Crossover object
Default SBX(prob=0.9, eta=15)

The recombination operator that combines two parent solutions to create offspring. The default is Simulated Binary Crossover (SBX), which mimics single-point crossover from binary-coded GAs but works in the real-valued (continuous) space.

from pymoo.operators.crossover.sbx import SBX
from pymoo.operators.crossover.pntx import TwoPointCrossover, SinglePointCrossover
from pymoo.operators.crossover.ux import UniformCrossover

# Default: SBX for continuous variables
NSGA2(crossover=SBX(prob=0.9, eta=15))

# Two-point crossover (for binary problems)
NSGA2(crossover=TwoPointCrossover())

# Uniform crossover
NSGA2(crossover=UniformCrossover(prob=0.5))

See SBX Crossover Parameters for full sub-parameter details.


mutation

   
Type Mutation object
Default PM(prob=None, eta=20)

The mutation operator randomly alters offspring variables to maintain genetic diversity and enable exploration beyond what crossover alone can reach. The default is Polynomial Mutation (PM) for real-valued variables.

When prob=None, pymoo automatically sets the per-variable mutation probability to 1/n_var, meaning on average one variable is mutated per individual - the standard recommendation.

from pymoo.operators.mutation.pm import PM
from pymoo.operators.mutation.bitflip import BitflipMutation

# Default: polynomial mutation for real variables
NSGA2(mutation=PM(prob=None, eta=20))

# Bit-flip mutation for binary variables
NSGA2(mutation=BitflipMutation())

See Polynomial Mutation Parameters for full sub-parameter details.


survival

   
Type Survival object
Default RankAndCrowding()

The survival selection strategy that decides which individuals move on to the next generation. NSGA-II’s defining feature is its RankAndCrowding survival, which works as follows:

  1. Merge parent and offspring populations into a combined pool of size 2N (or pop_size + n_offsprings).
  2. Sort all individuals by non-dominated rank (Pareto front 1, 2, 3…).
  3. Fill the next generation front-by-front until adding another full front would exceed pop_size.
  4. In the splitting front, select individuals with the highest crowding distance to maintain diversity.
from pymoo.operators.survival.rank_and_crowding import RankAndCrowding

NSGA2(survival=RankAndCrowding())

See RankAndCrowding Survival Parameters for crowding metric variants.


output

   
Type Output object
Default MultiObjectiveOutput()

Controls what information is printed to the console during optimization when verbose=True. The default multi-objective output shows generation number, evaluations, constraint violations, non-dominated solution count, and convergence indicators.

from pymoo.util.display.multi import MultiObjectiveOutput

NSGA2(output=MultiObjectiveOutput())

See Verbose Output - Column Meanings for a detailed explanation of each printed column.


n_offsprings

   
Type int or None
Default None

The number of offspring to generate per generation. When None, defaults to pop_size, implementing the standard (mu + lambda) strategy where the combined pool each generation has size 2 x pop_size.

Setting n_offsprings < pop_size creates a steady-state variant where only a fraction of the population is replaced per generation - useful for reducing the number of function evaluations per iteration at the cost of slower convergence per generation.

# Default: generate pop_size offspring each generation (full generational)
NSGA2(pop_size=100, n_offsprings=None)

# Steady-state: generate 10 offspring per generation
NSGA2(pop_size=100, n_offsprings=10)

Note: Using a small n_offsprings (like 10-20% of pop_size) on simple problems often leads to faster wall-clock convergence. On complex multimodal problems, full generational replacement (n_offsprings=None) is usually better.


eliminate_duplicates

   
Type bool or ElementwiseDuplicateElimination object
Default True

Whether to remove duplicate solutions from the offspring pool before survival selection. Duplicates are detected in the variable space (X), not the objective space.

  • True - use default duplicate detection (compares variable arrays element-wise within a small floating-point tolerance).
  • False - disable duplicate elimination entirely (faster, but may harm diversity).
  • A custom DuplicateElimination object - define your own equality logic (useful for permutation, mixed, or custom variable types).
from pymoo.core.duplicate import ElementwiseDuplicateElimination
import numpy as np

# Default: eliminate duplicates
NSGA2(eliminate_duplicates=True)

# Disable
NSGA2(eliminate_duplicates=False)

# Custom: consider two solutions equal if within 1e-4
class MyDuplicateElimination(ElementwiseDuplicateElimination):
    def is_equal(self, a, b):
        return np.allclose(a.X, b.X, atol=1e-4)

NSGA2(eliminate_duplicates=MyDuplicateElimination())

repair

   
Type Repair object or None
Default None

An optional repair operator applied to offspring after mutation and before evaluation. Its purpose is to fix infeasible or structurally invalid solutions that violate problem-specific constraints that the standard bounds-based operators cannot handle automatically.

Common use cases include:

  • Ensuring the sum of variables equals a fixed value (e.g., portfolio weights summing to 1).
  • Enforcing ordering constraints in scheduling problems.
  • Clamping or projecting solutions onto a feasible manifold.
from pymoo.core.repair import Repair
import numpy as np

class NormalizeRepair(Repair):
    """Repair operator that ensures all variables sum to 1.0."""
    def _do(self, problem, X, **kwargs):
        # X shape: (n_individuals, n_var)
        row_sums = X.sum(axis=1, keepdims=True)
        row_sums[row_sums == 0] = 1.0
        return X / row_sums

algorithm = NSGA2(
    pop_size=100,
    repair=NormalizeRepair()
)

Note: The repair operator runs inside the mating loop on the batch of generated offspring. It does not run on the initial population - if you need to repair the initial population too, use a custom Sampling class that generates valid solutions directly.


mating

   
Type Mating object or None
Default None

An optional mating object that bundles selection, crossover, mutation, and repair into a single configurable pipeline. When None, NSGA-II constructs its own default Mating object internally from the individual operator kwargs.

Providing a custom Mating object is an advanced option that allows you to override the entire offspring-generation pipeline in one step, rather than supplying operators one by one.

from pymoo.core.mating import Mating
from pymoo.operators.crossover.sbx import SBX
from pymoo.operators.mutation.pm import PM
from pymoo.operators.selection.tournament import TournamentSelection
from pymoo.algorithms.moo.nsga2 import binary_tournament

custom_mating = Mating(
    selection=TournamentSelection(func_comp=binary_tournament),
    crossover=SBX(prob=0.9, eta=15),
    mutation=PM(prob=None, eta=20),
    repair=None,
    eliminate_duplicates=True,
    n_max_iterations=100   # max tries to fill offspring pool without duplicates
)

algorithm = NSGA2(pop_size=100, mating=custom_mating)

Warning: When mating is explicitly provided, the individual selection, crossover, mutation, and repair kwargs at the NSGA2(...) level are ignored. The mating object takes full precedence. Use one approach or the other, not both.


Operator Sub-Parameters

SBX Crossover Parameters

Class: pymoo.operators.crossover.sbx.SBX

Simulated Binary Crossover (SBX) creates offspring that statistically mimic binary crossover behavior in real-valued search spaces, preserving the mean of parents while controlling spread via the distribution index eta.

Parameter Type Default Description
prob float 0.9 Probability that crossover is applied to a given mating pair. 0.9 means 90% of pairs undergo recombination; the remaining 10% are copied to the offspring pool unchanged.
eta float 15 Distribution index. Controls how closely offspring resemble their parents. Low eta (e.g., 2-5) -> offspring spread far from parents (exploration). High eta (e.g., 20-30) -> offspring stay close to parents (exploitation).
prob_var float 0.5 Per-variable crossover probability. Only applies when the pair-level prob is triggered. Each variable is independently recombined with this probability. Setting to 1.0 means all variables are always crossed.
vtype type float Variable type (float for continuous, int for integer-encoded variables).
from pymoo.operators.crossover.sbx import SBX

# High exploration (eta=5)
crossover = SBX(prob=0.9, eta=5)

# Conservative exploitation (eta=30), lower crossover rate
crossover = SBX(prob=0.7, eta=30)

# Cross every variable each time (prob_var=1.0)
crossover = SBX(prob=0.9, eta=15, prob_var=1.0)

Choosing eta:

eta Value Offspring Distribution Use Case
2 - 5 Widely spread (exploratory) Early stages, multimodal problems, diverse Pareto fronts
10 - 15 Moderate spread General purpose - good starting point for most problems
20 - 30 Tightly clustered Fine-tuning, convergence refinement, smooth Pareto fronts

Polynomial Mutation Parameters

Class: pymoo.operators.mutation.pm.PM

Polynomial Mutation perturbs individual variables by a small amount drawn from a polynomial probability distribution, always staying within the variable’s defined lower and upper bounds.

Parameter Type Default Description
prob float or None None Probability of mutating any given variable. None auto-sets this to 1/n_var, which ensures on average one variable per individual is mutated - the standard recommendation.
eta float 20 Distribution index for mutation. Low eta -> large perturbations (exploration, escaping local optima). High eta -> small perturbations (refinement near the current solution).
prob_var float or None None Alternative way to set the per-variable mutation probability (equivalent to prob).
from pymoo.operators.mutation.pm import PM

# Default: auto probability, eta=20
mutation = PM(prob=None, eta=20)

# Explicit mutation rate with higher spread
mutation = PM(prob=0.1, eta=10)

# Conservative mutation (very small perturbations)
mutation = PM(prob=0.05, eta=30)

Choosing eta:

eta Value Perturbation Size Use Case
5 - 10 Large jumps Escaping local optima, rugged search spaces
15 - 20 Moderate (default) General purpose
25 - 40 Very small steps Fine local search, smooth objective landscapes

Tournament Selection Parameters

Class: pymoo.operators.selection.tournament.TournamentSelection

Parameter Type Default Description
func_comp callable binary_tournament The comparison function used to determine the tournament winner. The default binary_tournament ranks by Pareto front first, then crowding distance as a tiebreaker. Supply a custom callable to implement different selection pressures.
pressure int 2 Tournament size - how many individuals compete per tournament. pressure=2 is binary tournament (one comparison). Higher values increase selection pressure toward high-rank solutions, reducing diversity.
from pymoo.operators.selection.tournament import TournamentSelection
from pymoo.algorithms.moo.nsga2 import binary_tournament

# Default
selection = TournamentSelection(func_comp=binary_tournament, pressure=2)

# Stronger selection pressure (3-way tournament)
selection = TournamentSelection(func_comp=binary_tournament, pressure=3)

RankAndCrowding Survival Parameters

Class: pymoo.operators.survival.rank_and_crowding.classes.RankAndCrowding

Parameter Type Default Description
nds NonDominatedSorting or None None The non-dominated sorting implementation. None uses the default fast non-dominated sort. Can be replaced with a custom NDS method.
crowding_func str or callable 'cd' The crowding metric used to distinguish solutions within the same Pareto front. See options below.

crowding_func Options:

Value Full Name Description
'cd' Crowding Distance Standard NSGA-II crowding distance - Manhattan distance in normalized objective space. Boundary solutions get infinite distance (always preserved). Proposed by Deb et al. (2002).
'pcd' Pruning Crowding Distance Recursively recalculates crowding distance as individuals are removed from the selection pool. Better diversity maintenance than standard CD. Proposed by Kukkonen & Deb (2006).
'ce' Crowding Entropy Entropy-based diversity measure. More robust in higher dimensions (3+ objectives). Proposed by Wang et al. (2009).
'mnn' M-Nearest Neighbors Distance to the M nearest neighbors in objective space, where M = number of objectives. Proposed by Kukkonen & Deb (2006).
'2nn' 2-Nearest Neighbors Simplified nearest-neighbor variant using only the 2 closest solutions. Computationally lighter than M-NN.
from pymoo.operators.survival.rank_and_crowding import RankAndCrowding

# Default NSGA-II crowding distance
NSGA2(survival=RankAndCrowding(crowding_func='cd'))

# Pruning crowding distance (generally better in practice)
NSGA2(survival=RankAndCrowding(crowding_func='pcd'))

# Entropy-based (useful for 3-objective problems)
NSGA2(survival=RankAndCrowding(crowding_func='ce'))

Inherited kwargs

NSGA-II inherits from GeneticAlgorithm, which inherits from Algorithm. The following are all recognized **kwargs passed through to the base Algorithm class:

Parameter Type Default Description
termination Termination or None None Termination criterion object. When None, the algorithm uses DefaultMultiObjectiveTermination internally. Can also be specified in minimize(). See DefaultMultiObjectiveTermination.
seed int or None None Random seed for reproducibility. When None, a random integer seed is chosen and stored in res.seed. If set in both the algorithm and minimize(), minimize() takes precedence.
verbose bool False Print generation-by-generation output to stdout. See Verbose Output - Column Meanings.
callback Callback or None None A Callback object whose notify(algorithm) method is called after every generation. Used to log metrics, save checkpoints, inspect or modify the algorithm mid-run.
display Display or None None Custom display object controlling what is printed when verbose=True. Overrides output.
save_history bool False If True, a deep snapshot of the algorithm state is stored after every generation into res.history. Essential for convergence analysis but increases memory usage linearly with n_gen.
return_least_infeasible bool False If True and no feasible solution exists at termination, return the individual(s) with the smallest constraint violation instead of None.
evaluator Evaluator or None None Custom evaluator object. Allows intercepting and wrapping the problem’s _evaluate call - useful for adding evaluation caches, logging, or custom parallelization strategies.
archive Archive or None None An optional external archive maintained alongside the population. Not used by default NSGA-II but can be attached for external elitism or solution archiving.
from pymoo.core.callback import Callback

class ConvergenceLogger(Callback):
    def __init__(self):
        super().__init__()
        self.data["n_nds"] = []
        self.data["n_evals"] = []

    def notify(self, algorithm):
        self.data["n_nds"].append(len(algorithm.opt))
        self.data["n_evals"].append(algorithm.evaluator.n_eval)

algorithm = NSGA2(
    pop_size=100,
    seed=42,
    verbose=True,
    save_history=True,
    callback=ConvergenceLogger()
)

minimize() Function Parameters

The minimize() function drives the optimization run. It accepts its own set of parameters and also passes unrecognized **kwargs through to the algorithm.

from pymoo.optimize import minimize

res = minimize(
    problem,
    algorithm,
    termination=None,
    seed=None,
    verbose=False,
    display=None,
    callback=None,
    return_least_infeasible=False,
    save_history=False,
    copy_algorithm=True,
    copy_termination=True,
)
Parameter Type Default Description
problem Problem (required) The problem instance to optimize.
algorithm Algorithm (required) The configured NSGA2 (or any other algorithm) object.
termination tuple or Termination or None None When to stop. Short tuples like ('n_gen', 200), ('n_eval', 10000), or ('time', '00:01:00') are accepted. When None, DefaultMultiObjectiveTermination is used.
seed int or None None Random seed for the run. Overrides any seed set on the algorithm object itself.
verbose bool False Print generation-by-generation progress. Overrides the verbose flag on the algorithm.
display Display or None None Custom display object for controlling printed output.
callback Callback or None None Callback invoked each generation. Overrides any callback set on the algorithm.
return_least_infeasible bool False Return the least constraint-violating solution if no feasible one is found at termination.
save_history bool False Save per-generation algorithm snapshots into res.history.
copy_algorithm bool True Deep-copy the algorithm object before running. Ensures two runs with the same algorithm object and same seed produce identical, independent results.
copy_termination bool True Deep-copy the termination criterion before running. Prevents state leakage if the same termination object is reused across runs.

Termination shortcut tuples:

Tuple Meaning
('n_gen', 200) Stop after 200 generations
('n_eval', 50000) Stop after 50,000 function evaluations
('time', '00:30:00') Stop after 30 minutes (HH:MM:SS format)
('xtol', 1e-8) Stop when design-space change drops below 1e-8
('ftol', 0.0025) Stop when objective-space change drops below 0.0025

DefaultMultiObjectiveTermination

When no termination criterion is provided to minimize(), NSGA-II uses DefaultMultiObjectiveTermination internally. Understanding its parameters is critical for tuning automatic stopping behavior.

Class: pymoo.termination.default.DefaultMultiObjectiveTermination

from pymoo.termination.default import DefaultMultiObjectiveTermination

termination = DefaultMultiObjectiveTermination(
    xtol=0.0005,
    cvtol=1e-8,
    ftol=0.005,
    n_skip=5,
    period=50,
    n_max_gen=1000,
    n_max_evals=100000,
)
Parameter Type Default Description
xtol float 0.0005 Design space tolerance. The algorithm stops when the maximum absolute change in decision variables across the sliding window is below this value. Measures stagnation in X.
cvtol float 1e-8 Constraint violation tolerance. Stops (on the CV criterion) when constraint violation falls below this threshold. For unconstrained problems this criterion is always satisfied and does not trigger premature termination.
ftol float 0.005 Objective space tolerance. Stops when the relative change in the normalized Pareto front (measured via changes in the ideal point, nadir point, and front spread) falls below this value across the sliding window.
n_skip int 5 How often (in generations) the termination criterion is evaluated. 1 evaluates every generation (most responsive). Higher values reduce computational overhead for the convergence check.
period int 50 Sliding window size. The number of past generations considered when computing tolerances. The maximum change over this window is used (worst-case), not just the last generation. Larger values make termination more conservative.
n_max_gen int 1000 Fallback maximum generations. An upper bound to prevent infinite runs if tolerance criteria are never met. Acts as a safety net.
n_max_evals int 100000 Fallback maximum evaluations. An upper bound on total function evaluations. Whichever of n_max_gen or n_max_evals is hit first triggers termination.

How termination is decided: The algorithm stops when all three conditions are satisfied within the sliding window (xtol AND cvtol AND ftol), OR when the fallback n_max_gen or n_max_evals is reached.

from pymoo.termination.default import DefaultMultiObjectiveTermination
from pymoo.optimize import minimize

# Stricter convergence, larger window, higher fallback
termination = DefaultMultiObjectiveTermination(
    xtol=1e-8,
    cvtol=1e-8,
    ftol=1e-6,
    period=100,
    n_skip=5,
    n_max_gen=5000,
    n_max_evals=500000,
)

res = minimize(problem, algorithm, termination, seed=1, verbose=True)

# Effectively disable ftol (rely only on xtol and n_max_gen):
termination = DefaultMultiObjectiveTermination(
    ftol=1.0,       # Very large value, never triggered
    n_max_gen=500,  # Rely on generation cap instead
)

Result Object

The Result object returned by minimize() contains all output from the optimization run.

Full attribute reference:

Attribute Type Description
res.X ndarray shape (n_nds, n_var) Decision variable values of the final non-dominated (Pareto-optimal) solutions. None if no feasible solution was found.
res.F ndarray shape (n_nds, n_obj) Objective values of the final Pareto front solutions, in the same order as res.X.
res.G ndarray shape (n_nds, n_constr) or None Inequality constraint values for the Pareto solutions. None if the problem is unconstrained.
res.H ndarray or None Equality constraint values for the Pareto solutions. None if no equality constraints exist.
res.CV ndarray shape (n_nds, 1) Constraint violation values. Always 0 for unconstrained problems.
res.opt Population The full Population object representing the final non-dominated set. Use .get("X", "F", "rank", "crowding") to extract specific attributes.
res.pop Population The entire final population (both dominated and non-dominated). Useful for post-hoc analysis.
res.algorithm Algorithm The algorithm object after optimization has completed. Contains the full final state.
res.problem Problem The problem instance that was solved.
res.history list[Algorithm] List of per-generation algorithm snapshots. Only populated if save_history=True. Length equals the number of generations run.
res.exec_time float Total wall-clock runtime in seconds.
res.start_time float Unix timestamp of when the run started.
res.end_time float Unix timestamp of when the run ended.
res.seed int The random seed actually used (useful when seed was auto-assigned from None).
res.success bool Whether the algorithm terminated successfully.
res.message str or None An optional termination message.
res.pf ndarray or None The true Pareto front, if known (provided by the problem). None for unknown fronts.
res.archive Archive or None External archive contents if an archive was attached to the algorithm.
res.data dict Data stored within the algorithm object during the run (e.g., from custom callbacks).
res = minimize(problem, algorithm, ('n_gen', 200), seed=1, save_history=True)

# Pareto front results
print(res.X)          # shape (n_nds, n_var)
print(res.F)          # shape (n_nds, n_obj)
print(res.CV)         # shape (n_nds, 1)

# Full population attributes
X_all   = res.pop.get("X")        # all individuals' variables
F_all   = res.pop.get("F")        # all individuals' objectives
ranks   = res.pop.get("rank")     # Pareto rank (0 = first front)
cd      = res.pop.get("crowding") # crowding distance

# Algorithm state
print(f"Generations run:  {res.algorithm.n_gen}")
print(f"Evaluations used: {res.algorithm.evaluator.n_eval}")
print(f"Wall time:        {res.exec_time:.2f}s")
print(f"Seed used:        {res.seed}")

# Convergence history (requires save_history=True)
for gen_snap in res.history:
    n_nds  = len(gen_snap.opt)
    n_eval = gen_snap.evaluator.n_eval
    print(f"Gen {gen_snap.n_gen}: {n_nds} non-dominated, {n_eval} evals")

Verbose Output

When verbose=True is passed to minimize(), NSGA-II prints a header and one row per generation:

n_gen | n_eval | n_nds | cv (min) | cv (avg) | eps      | indicator
===========================================================================================
1     | 100    | 6     | 0.0      | 0.0      | -        | -
2     | 200    | 9     | 0.0      | 0.0      | 2.0E+00  | ideal
3     | 300    | 14    | 0.0      | 0.0      | 1.5E+00  | nadir
Column Description
n_gen Current generation number (starts at 1).
n_eval Cumulative number of objective function evaluations so far.
n_nds Number of non-dominated solutions (Pareto front 1 size) in the current population. A steadily growing n_nds is a sign of healthy convergence.
cv (min) Minimum constraint violation in the current population. Only shown for constrained problems. 0.0 means at least one individual is feasible.
cv (avg) Average constraint violation across the current population. Only shown for constrained problems.
eps The convergence metric value at this generation. Measures how much the Pareto front has moved since the previous evaluation window. Computed as the maximum of: change in ideal point, change in nadir point, and change in front spread. Lower values mean closer to convergence. - means not yet computed (within n_skip interval).
indicator Which component (ideal, nadir, or f) drove the eps value in this generation. Tells you why the algorithm has or has not converged yet.

Sampling Options Reference

Class Module Variable Type Description
FloatRandomSampling pymoo.operators.sampling.rnd Real (float) Uniform random sampling within problem bounds. Default for continuous problems.
BinaryRandomSampling pymoo.operators.sampling.rnd Binary (0/1) Randomly assigns 0 or 1 to each variable with equal probability.
IntegerRandomSampling pymoo.operators.sampling.rnd Integer Random integers sampled uniformly within bounds.
PermutationRandomSampling pymoo.operators.sampling.rnd Permutation Generates random orderings (e.g., for TSP-type problems).
LHS pymoo.operators.sampling.lhs Real Latin Hypercube Sampling - ensures better space-filling coverage. Strongly recommended when pop_size is small or n_var is large.
from pymoo.operators.sampling.lhs import LHS
NSGA2(sampling=LHS())    # Better initial coverage than pure random

Crossover Options Reference

Class Module Variable Type Notes
SBX pymoo.operators.crossover.sbx Real Simulated Binary Crossover. Default for continuous problems. Most widely used.
SinglePointCrossover pymoo.operators.crossover.pntx Binary / Integer Cuts both parents at one point and swaps tails.
TwoPointCrossover pymoo.operators.crossover.pntx Binary / Integer Cuts at two points and swaps the middle segment.
UniformCrossover (UX) pymoo.operators.crossover.ux Binary Each gene independently inherited from either parent with probability 0.5.
HalfUniformCrossover (HUX) pymoo.operators.crossover.hux Binary Only swaps the differing genes between parents; preserves shared bits.
OrderCrossover (OX) pymoo.operators.crossover.ox Permutation Preserves relative order of elements. Standard for TSP-type problems.
EdgeRecombinationCrossover (ERX) pymoo.operators.crossover.erx Permutation Uses edge tables for tour recombination in TSP-style problems.

Mutation Options Reference

Class Module Variable Type Notes
PM pymoo.operators.mutation.pm Real Polynomial Mutation. Default for continuous problems.
BitflipMutation pymoo.operators.mutation.bitflip Binary Flips each bit independently with probability prob.
InversionMutation pymoo.operators.mutation.inversion Permutation Reverses a randomly selected sub-sequence of the permutation.

Performance Indicators

After optimization, pymoo provides several indicators to measure the quality of the Pareto front found. These are especially useful for benchmarking and convergence analysis.

Overview of Indicators

Indicator Class Requires Optimization Direction Description
GD GD True Pareto front Minimize Generational Distance - average distance from each found solution to the nearest point on the true Pareto front. Measures convergence only, not diversity.
GD+ GDPlus True Pareto front Minimize Modified GD using a Pareto-compliant distance. Preferred over plain GD.
IGD IGD True Pareto front Minimize Inverted GD - average distance from each true Pareto front point to the nearest found solution. Measures both convergence and coverage.
IGD+ IGDPlus True Pareto front Minimize Pareto-compliant version of IGD. Preferred over plain IGD in most research.
Hypervolume HV Reference point Maximize Volume of objective space dominated by found solutions relative to a reference point. Pareto-compliant; measures convergence and spread simultaneously. No true Pareto front needed.

Usage

from pymoo.indicators.igd import IGD
from pymoo.indicators.igd_plus import IGDPlus
from pymoo.indicators.gd import GD
from pymoo.indicators.hv import HV
import numpy as np

res = minimize(problem, algorithm, ('n_gen', 200), seed=1, save_history=True)

# --- Pareto-front based indicators (need the true Pareto front) ---
pf = problem.pareto_front()       # Only available for test problems

igd      = IGD(pf)
igd_plus = IGDPlus(pf)
gd       = GD(pf)

print("IGD:  ", igd.do(res.F))        # lower is better
print("IGD+: ", igd_plus.do(res.F))   # lower is better
print("GD:   ", gd.do(res.F))         # lower is better

# --- Hypervolume (needs a reference point, not the Pareto front) ---
# ref_point must be strictly dominated by all Pareto front points
ref_point = np.array([1.1, 1.1])
hv = HV(ref_point=ref_point)
print("HV:   ", hv.do(res.F))         # higher is better

# --- Automatic reference point from found solutions ---
max_F = res.F.max(axis=0)
ref_point_auto = max_F * 1.1
hv_auto = HV(ref_point=ref_point_auto)
print("HV (auto ref):", hv_auto.do(res.F))

# --- Convergence curve over generations ---
n_evals = []
hv_over_time = []

for gen_snap in res.history:
    opt_F = gen_snap.opt.get("F")
    n_evals.append(gen_snap.evaluator.n_eval)
    hv_over_time.append(hv.do(opt_F))

import matplotlib.pyplot as plt
plt.plot(n_evals, hv_over_time, '-o', markersize=3)
plt.xlabel("Function Evaluations")
plt.ylabel("Hypervolume")
plt.title("NSGA-II Convergence")
plt.show()

Indicator Selection Guide

Situation Recommended Indicator
True Pareto front is known (test problems) IGD+ (Pareto-compliant, measures both convergence and spread)
True Pareto front is unknown (real problems) Hypervolume (needs only a reference point)
Comparing convergence speed only GD+
Need a single ranking number Hypervolume (most theoretically sound)
Computational budget is tight (many-obj) IGD (faster than exact Hypervolume in high dimensions)

Parallelization

Expensive objective functions are common in real-world problems (simulations, FEM, CFD, etc.). pymoo supports several parallelization strategies that work seamlessly with NSGA-II without modifying the algorithm itself.

Strategy 1: Vectorized Evaluation (Fastest - No Overhead)

Implement _evaluate to accept a full batch of solutions at once (X is a 2D array). This is the default mode and relies on NumPy’s vectorized operations.

import numpy as np
from pymoo.core.problem import Problem

class VectorizedProblem(Problem):
    def __init__(self):
        super().__init__(n_var=10, n_obj=2, xl=0.0, xu=1.0)

    def _evaluate(self, X, out, *args, **kwargs):
        # X shape: (pop_size, n_var) - entire population at once
        f1 = np.sum(X ** 2, axis=1)
        f2 = np.sum((X - 1) ** 2, axis=1)
        out["F"] = np.column_stack([f1, f2])

Strategy 2: Python Multiprocessing via starmap

Use Python’s built-in multiprocessing.Pool to evaluate individuals in parallel. Best for CPU-bound Python evaluations.

from multiprocessing.pool import Pool
from pymoo.core.problem import ElementwiseProblem
from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.optimize import minimize
from pymoo.problems.parallelization import StarMapParallelization

class MyExpensiveProblem(ElementwiseProblem):
    def __init__(self, **kwargs):
        super().__init__(n_var=5, n_obj=2, xl=0.0, xu=1.0, **kwargs)

    def _evaluate(self, x, out, *args, **kwargs):
        import time
        time.sleep(0.01)  # Simulate expensive evaluation
        out["F"] = [float(sum(x**2)), float(sum((x-1)**2))]

# Run with 4 parallel workers
n_proccess = 4
pool = Pool(n_proccess)
runner = StarMapParallelization(pool.starmap)

problem = MyExpensiveProblem(elementwise_runner=runner)
algorithm = NSGA2(pop_size=100)
res = minimize(problem, algorithm, ('n_gen', 50), seed=1)
pool.close()

Strategy 3: Joblib (Simpler API, Cross-Platform)

from joblib import Parallel, delayed
import numpy as np
from pymoo.core.problem import Problem
from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.optimize import minimize

class JoblibProblem(Problem):
    def __init__(self, n_jobs=-1):
        super().__init__(n_var=10, n_obj=2, xl=0.0, xu=1.0)
        self.n_jobs = n_jobs

    def _evaluate(self, X, out, *args, **kwargs):
        def eval_single(x):
            return [float(np.sum(x ** 2)), float(np.sum((x - 1) ** 2))]

        F = Parallel(n_jobs=self.n_jobs)(
            delayed(eval_single)(X[i]) for i in range(len(X))
        )
        out["F"] = np.array(F)

problem = JoblibProblem(n_jobs=8)
algorithm = NSGA2(pop_size=100)
res = minimize(problem, algorithm, ('n_gen', 200), seed=1)

Strategy 4: Custom Evaluator (Dask / External Cluster)

For cluster-based or distributed evaluations, implement a custom Evaluator:

from pymoo.core.evaluator import Evaluator
from pymoo.algorithms.moo.nsga2 import NSGA2

class AsyncEvaluator(Evaluator):
    def __init__(self, submit_fn, gather_fn):
        super().__init__()
        self.submit_fn = submit_fn
        self.gather_fn = gather_fn

    def _eval(self, problem, pop, **kwargs):
        # Submit all evaluations asynchronously
        futures = [self.submit_fn(problem._evaluate_single, ind.X) for ind in pop]
        results = self.gather_fn(futures)
        for ind, res in zip(pop, results):
            ind.F = res
        return pop

# Usage with Dask (example)
# from dask.distributed import Client
# client = Client()
# evaluator = AsyncEvaluator(client.submit, client.gather)
# algorithm = NSGA2(pop_size=100, evaluator=evaluator)

Parallelization Strategy Comparison

Strategy Best For Overhead Complexity
Vectorized (numpy) Pure Python/NumPy math None Low
starmap / multiprocessing CPU-bound Python functions Medium (IPC) Low
Joblib CPU-bound, cross-platform Medium Low
Custom Evaluator + Dask Distributed/cluster simulation High (network) High

When NOT to Use NSGA-II

NSGA-II is an excellent default choice, but it has known limitations:

Scenario Problem Recommended Alternative
4+ objectives NSGA-II’s crowding distance degrades badly in high-dimensional objective spaces - most solutions end up on the first front, destroying selection pressure. NSGA-III, MOEA/D, or AGE-MOEA
Highly multimodal / many local Pareto fronts NSGA-II can converge to local fronts and struggles to escape them. RVEA, AGE-MOEA2, or restart strategies
Very expensive evaluations (low budget) NSGA-II needs many evaluations. With fewer than ~500 evaluations, surrogate-assisted methods outperform it. Bayesian optimization, surrogate-based MOO
Continuous gradient available NSGA-II ignores gradient information entirely. Multi-objective gradient descent, NSGA-II + gradient-guided operators
Discrete / combinatorial at scale SBX and PM are designed for continuous spaces. Binary/permutation adaptations exist but underperform specialized methods. BRKGA (permutations), problem-specific EAs
Dynamic problems (objectives change over time) Standard NSGA-II has no mechanism to track moving Pareto fronts. D-NSGA-II (pymoo.algorithms.moo.dnsga2)
Known preference or reference point When the decision-maker targets a specific region of the Pareto front, NSGA-II explores the full front wastefully. R-NSGA-II (pymoo.algorithms.moo.rnsga2)

Full Configuration Examples

Example 1: Default Configuration (Continuous Real Variables)

from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.problems import get_problem
from pymoo.optimize import minimize
from pymoo.visualization.scatter import Scatter

problem = get_problem("zdt1")
algorithm = NSGA2(pop_size=100)

res = minimize(problem, algorithm, ('n_gen', 200), seed=1, verbose=True)

Scatter().add(problem.pareto_front(), plot_type="line", color="black") \
         .add(res.F, facecolor="none", edgecolor="red").show()

Example 2: Customized Continuous Problem (Steady-State + LHS + Pruning Crowding)

from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.operators.crossover.sbx import SBX
from pymoo.operators.mutation.pm import PM
from pymoo.operators.sampling.lhs import LHS
from pymoo.operators.survival.rank_and_crowding import RankAndCrowding
from pymoo.optimize import minimize

algorithm = NSGA2(
    pop_size=150,
    n_offsprings=30,
    sampling=LHS(),
    crossover=SBX(prob=0.9, eta=20),
    mutation=PM(prob=None, eta=15),
    survival=RankAndCrowding(crowding_func='pcd'),
    eliminate_duplicates=True,
)

res = minimize(problem, algorithm, ('n_gen', 300), seed=42, verbose=True)

Example 3: Binary Variable Problem (ZDT5)

from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.operators.crossover.pntx import TwoPointCrossover
from pymoo.operators.mutation.bitflip import BitflipMutation
from pymoo.operators.sampling.rnd import BinaryRandomSampling
from pymoo.problems import get_problem
from pymoo.optimize import minimize

problem = get_problem("zdt5")

algorithm = NSGA2(
    pop_size=100,
    sampling=BinaryRandomSampling(),
    crossover=TwoPointCrossover(),
    mutation=BitflipMutation(),
    eliminate_duplicates=True
)

res = minimize(problem, algorithm, ('n_gen', 500), seed=1, verbose=False)

Example 4: Constrained Problem with Repair Operator

import numpy as np
from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.core.problem import ElementwiseProblem
from pymoo.core.repair import Repair
from pymoo.optimize import minimize

class PortfolioProblem(ElementwiseProblem):
    """Two-objective portfolio: maximize return, minimize risk. Weights must sum to 1."""
    def __init__(self):
        super().__init__(n_var=5, n_obj=2, xl=0.0, xu=1.0)

    def _evaluate(self, x, out, *args, **kwargs):
        out["F"] = [-np.dot(x, [0.10, 0.20, 0.15, 0.12, 0.18]),
                     np.dot(x, [0.05, 0.08, 0.07, 0.06, 0.09])]

class SumToOneRepair(Repair):
    def _do(self, problem, X, **kwargs):
        row_sums = X.sum(axis=1, keepdims=True)
        row_sums[row_sums == 0] = 1.0
        return X / row_sums

algorithm = NSGA2(
    pop_size=100,
    repair=SumToOneRepair(),
    eliminate_duplicates=True,
    return_least_infeasible=True,
)

res = minimize(PortfolioProblem(), algorithm, ('n_gen', 200), seed=1, verbose=True)
print("Optimal weights:\n", res.X)
print("Return / Risk tradeoffs:\n", res.F)

Example 5: Callbacks, History, and Performance Indicators

from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.core.callback import Callback
from pymoo.indicators.hv import HV
from pymoo.indicators.igd_plus import IGDPlus
from pymoo.problems import get_problem
from pymoo.optimize import minimize
import numpy as np
import matplotlib.pyplot as plt

class HVCallback(Callback):
    """Track hypervolume every generation."""
    def __init__(self, ref_point):
        super().__init__()
        self.hv_indicator = HV(ref_point=ref_point)
        self.data["hv"] = []
        self.data["n_eval"] = []

    def notify(self, algorithm):
        F = algorithm.opt.get("F")
        if F is not None and len(F) > 0:
            self.data["hv"].append(self.hv_indicator.do(F))
            self.data["n_eval"].append(algorithm.evaluator.n_eval)

problem = get_problem("zdt1")
ref_point = np.array([1.1, 1.1])
callback = HVCallback(ref_point=ref_point)

algorithm = NSGA2(pop_size=100, eliminate_duplicates=True, callback=callback)

res = minimize(problem, algorithm, ('n_gen', 200), seed=1, verbose=True, save_history=True)

# Plot convergence
plt.plot(callback.data["n_eval"], callback.data["hv"], '-o', markersize=3)
plt.xlabel("Function Evaluations")
plt.ylabel("Hypervolume")
plt.title("NSGA-II Convergence on ZDT1")
plt.show()

# Final performance
igd_plus = IGDPlus(problem.pareto_front())
hv       = HV(ref_point=ref_point)
print(f"IGD+:        {igd_plus.do(res.F):.6f}")
print(f"Hypervolume: {hv.do(res.F):.6f}")
print(f"Exec time:   {res.exec_time:.2f}s")
print(f"Total evals: {res.algorithm.evaluator.n_eval}")

Example 6: Smart Auto-Termination

from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.termination.default import DefaultMultiObjectiveTermination
from pymoo.problems import get_problem
from pymoo.optimize import minimize

problem = get_problem("zdt3")

termination = DefaultMultiObjectiveTermination(
    xtol=1e-8,
    cvtol=1e-8,
    ftol=0.001,
    period=30,
    n_skip=5,
    n_max_gen=2000,
    n_max_evals=200000,
)

algorithm = NSGA2(pop_size=100, eliminate_duplicates=True)

res = minimize(problem, algorithm, termination, seed=1, verbose=True)
print(f"Stopped at generation: {res.algorithm.n_gen}")
print(f"Non-dominated solutions found: {len(res.F)}")

Quick Reference Card

NSGA2(
    # --- Population & Offspring ---
    pop_size              = 100,         # Population size
    n_offsprings          = None,        # Offspring per gen (None = pop_size)

    # --- Operators ---
    sampling              = FloatRandomSampling(),    # or LHS(), BinaryRandom, ndarray
    selection             = TournamentSelection(
                              func_comp=binary_tournament,
                              pressure=2              # tournament size
                            ),
    crossover             = SBX(
                              prob=0.9,               # crossover probability
                              eta=15,                 # distribution index
                              prob_var=0.5            # per-variable probability
                            ),
    mutation              = PM(
                              prob=None,              # per-var rate (None = 1/n_var)
                              eta=20                  # distribution index
                            ),
    survival              = RankAndCrowding(
                              crowding_func='cd'      # 'cd','pcd','ce','mnn','2nn'
                            ),
    repair                = None,                     # Repair operator (optional)
    mating                = None,                     # Full mating pipeline (optional)

    # --- Diversity & Quality ---
    eliminate_duplicates  = True,

    # --- Display ---
    output                = MultiObjectiveOutput(),

    # --- Inherited from Algorithm base ---
    termination           = None,        # or DefaultMultiObjectiveTermination(...)
    seed                  = None,        # int for reproducibility
    verbose               = False,       # print per-generation output
    callback              = None,        # Callback object
    save_history          = False,       # store per-gen snapshots in res.history
    return_least_infeasible = False,     # return best infeasible if none feasible
    display               = None,        # custom Display object
    evaluator             = None,        # custom Evaluator (e.g., for parallelism)
    archive               = None,        # external archive
)

Documentation compiled from the official pymoo documentation (v0.6.1.6), source code at github.com/anyoptimization/pymoo, and Deb et al. (2002). For the latest API changes always refer to the pymoo GitHub repository.

CC BY NSGA-II Optimization Reference © Muthukrishnan Anand
Licensed under a Creative Commons Attribution 4.0 International License (CC BY 4.0). You may share and adapt this work with attribution. Built with using Claude Code.