Component-Based Architecture¶
jmetal.component assembles algorithms from small, independently testable components instead of
defining them by subclassing. It mirrors jMetal Java's jmetal-component module and lives
alongside jmetal.algorithm, which keeps working unchanged -- nothing here replaces the classic
algorithms, and no existing example, notebook, or user code is affected.
This page currently covers evolutionary algorithms (EA/MOEA). A PSO template and catalogue are planned but not implemented yet.
For a hands-on walkthrough of every configuration described below -- final fronts, quality
indicator values, and the observer pattern in action -- see the
notebooks/NSGAIIComponentBased.ipynb,
notebooks/SMSEMOAComponentBased.ipynb
and
notebooks/MOEADComponentBased.ipynb
notebooks.
Why¶
The classic algorithms hard-wire their steps as methods on a class hierarchy: GeneticAlgorithm
defines create_initial_solutions(), evaluate(), selection(), reproduction(), and
NSGAII.replacement() overrides the base class's replacement step. Swapping one step for another
means subclassing. A component-based algorithm instead takes six collaborators as constructor
arguments, so any of them can be swapped independently without touching the algorithm's control
flow at all.
The six components¶
| Component | Responsibility | Protocol method(s) |
|---|---|---|
SolutionsCreation |
Creates the initial population | create() -> list[S] |
Evaluation |
Evaluates a population against a problem | evaluate(list[S]) -> list[S], computed_evaluations() -> int |
Termination |
Decides when to stop | is_met(status: Mapping) -> bool |
Selection |
Builds a mating pool from the population | select(list[S]) -> list[S] |
Variation |
Turns a mating pool into offspring | variate(...) -> list[S], mating_pool_size() -> int, offspring_population_size() -> int |
Replacement |
Selects survivors from parents and offspring | replace(current, offspring) -> list[S] |
Each is a typing.Protocol
(except Replacement, an ABC -- see below):
structural typing means any object exposing the right method already satisfies the contract, with
no need to inherit from anything. A bare function wrapped in a small object, a lambda, or an
existing class all work.
Replacement is the one exception: the three replacement strategies that already existed in
jmetal.operator.replacement (RankingAndDensityEstimatorReplacement,
RankingAndCrowdingDistanceReplacement, SMSEMOAReplacement) only shared their replace() method
by convention, with no common base to type against. jmetal.operator.replacement.Replacement is a
minimal ABC that fixes that, and jmetal.component.catalogue.ea.replacement re-exports it
directly -- no adapter needed, since the existing classes already match the shape the component
model expects.
The EvolutionaryAlgorithm template¶
jmetal.component.algorithm.evolutionary_algorithm.EvolutionaryAlgorithm is a direct translation
of jMetal Java's EvolutionaryAlgorithm template. Its run() is exactly:
population = solutions_creation.create()
population = evaluation.evaluate(population)
while not termination.is_met(state):
mating_population = selection.select(population)
offspring_population = variation.variate(population, mating_population)
offspring_population = evaluation.evaluate(offspring_population)
population = replacement.replace(population, offspring_population)
Unlike jmetal.core.algorithm.Algorithm, it does not inherit from threading.Thread: nothing in
jMetalPy calls start()/join() on an algorithm, so the template skips that coupling instead of
carrying it for no benefit.
Building NSGA-II¶
build_nsgaii() is a factory function, not a chainable builder class. jMetal Java's
NSGAIIBuilder uses .setX().build() because Java has no keyword arguments; Python already does,
so a function with keyword-only overrides gives the same flexibility without an intermediate
mutable object:
from jmetal.component.algorithm.multiobjective.nsgaii import build_nsgaii
from jmetal.operator.crossover import SBXCrossover
from jmetal.operator.mutation import PolynomialMutation
from jmetal.problem import ZDT1
problem = ZDT1()
algorithm = build_nsgaii(
problem,
population_size=100,
offspring_population_size=100,
crossover=SBXCrossover(probability=1.0, distribution_index=20),
mutation=PolynomialMutation(probability=1.0 / problem.number_of_variables(), distribution_index=20),
)
algorithm.run()
front = algorithm.result()
Every default -- selection, variation, replacement, termination -- can be overridden with a
keyword argument, and every existing observer in jmetal.util.observer
(ProgressBarObserver, VisualizerObserver, WriteFrontToFileObserver, ...) works against
algorithm.observable unchanged, since AlgorithmState.as_dict() produces the same
"PROBLEM"/"EVALUATIONS"/"SOLUTIONS"/"COMPUTING_TIME"-keyed mapping the classic algorithms
already notify with.
Building SMS-EMOA¶
SMS-EMOA is very similar to NSGA-II: build_smsemoa() follows the exact same factory-function
pattern and reuses four of the six components unchanged (RandomSolutionsCreation,
SequentialEvaluation, TerminationByEvaluations, CrossoverAndMutationVariation). It only
differs in mating selection -- RandomSelection rather than tournament, since SMS-EMOA relies on
its replacement strategy alone to drive convergence -- and in replacement -- SMSEMOAReplacement,
which ranks the merged population, keeps every front but the last whole, and prunes the last front
by hypervolume contribution rather than crowding distance:
from jmetal.component.algorithm.multiobjective.smsemoa import build_smsemoa
from jmetal.operator.crossover import SBXCrossover
from jmetal.operator.mutation import PolynomialMutation
from jmetal.problem import ZDT1
problem = ZDT1()
algorithm = build_smsemoa(
problem,
population_size=100,
crossover=SBXCrossover(probability=1.0, distribution_index=20),
mutation=PolynomialMutation(probability=1.0 / problem.number_of_variables(), distribution_index=20),
)
algorithm.run()
front = algorithm.result()
Unlike build_nsgaii(), there is no offspring_population_size parameter: SMS-EMOA is
steady-state by definition (Beume et al., 2007) and always produces exactly one offspring per
generation, matching jmetal.algorithm.multiobjective.smsemoa.SMSEMOA, which hardcodes the same
value. Every other default -- selection, variation, replacement, termination -- can be
overridden the same way as build_nsgaii()'s, and archive= is supported identically (see
External archives below) -- both factories share the same
EvolutionaryAlgorithm template, so nothing archive-specific needed to change.
Building MOEA/D¶
build_moead() (classic, any crossover) and build_moead_de() (differential evolution) build on
the exact same EvolutionaryAlgorithm template as build_nsgaii()/build_smsemoa() -- no
modification needed there. What MOEA/D actually needs that NSGA-II/SMS-EMOA don't: Selection,
Variation and Replacement all need to agree, every iteration, on which subproblem is being
processed and whether this iteration's mating pool and replacement scan are scoped to that
subproblem's neighborhood or the whole population -- neither concept exists in the generic
select()/variate()/replace() signatures, nor should it (it's specific to MOEA/D, not a general
evolutionary-algorithm concern). catalogue/ea/moead.py's MOEADContext is a small object
constructed once per run and passed by reference into MOEADSelection, MOEADReplacement and (for
the DE variant) DifferentialEvolutionCrossoverVariation, each of which reads it without the
template needing to know it exists -- mirroring how jMetal Java's own component-based MOEA/D
(MOEADBuilder/MOEADDEBuilder in jmetal-component) shares a SequenceGenerator<Integer> the
same way:
from jmetal.component.algorithm.multiobjective.moead import build_moead, build_moead_de
from jmetal.operator.crossover import SBXCrossover
from jmetal.operator.mutation import PolynomialMutation
from jmetal.problem import ZDT1
problem = ZDT1()
# Classic: any crossover, defaults to PenaltyBoundaryIntersection aggregation.
algorithm = build_moead(
problem,
population_size=100,
crossover=SBXCrossover(probability=1.0, distribution_index=20),
mutation=PolynomialMutation(probability=1.0 / problem.number_of_variables(), distribution_index=20),
)
# MOEA/D-DE: differential-evolution crossover, defaults to Tschebycheff aggregation.
algorithm = build_moead_de(
problem,
population_size=100,
mutation=PolynomialMutation(probability=1.0 / problem.number_of_variables(), distribution_index=20),
cr=1.0,
f=0.5,
)
algorithm.run()
front = algorithm.result()
Both reuse RandomSolutionsCreation, SequentialEvaluation/SequentialEvaluationWithArchive and
TerminationByEvaluations unchanged, and support archive= identically to build_nsgaii()/
build_smsemoa(). population_size doubles as the number of subproblems (one weight vector per
population slot); for 3+ objectives the weight vectors are read from a file in weight_files_path
(default: this repository's bundled resources/MOEAD_weights/) -- 2-objective weight vectors are
generated analytically and need no file.
A correction on record. This page previously carried a note claiming MOEA/D "does not fit the
component model well," attributed to unverified Java design notes. Direct
investigation of jmetal-component found the opposite -- MOEADBuilder/MOEADDEBuilder already
exist there, building the same generic template with no modification -- so the note was corrected
rather than repeated here.
A step further on reproducibility. Every MOEA/D-specific random decision (which subproblem,
which neighborhood-vs-population scope, mating-pool sampling, the replacement scan) draws
exclusively from the rng passed to build_moead()/build_moead_de(), never from the global
random/numpy.random state -- a deliberate departure from the classic
jmetal.algorithm.multiobjective.moead.MOEAD, which mixes three incompatible random sources (global
random, global legacy numpy.random, and each operator's own np.random.Generator) and can
therefore never be made reproducible from a single seed. Getting there also meant reaching one level
deeper than build_nsgaii()/build_smsemoa() did: Problem.create_solution() gained an optional
rng parameter (defaulting to the same global-state behavior as before, so nothing already relying
on random.seed() breaks), and RandomSolutionsCreation now forwards its own rng into it -- so
population creation is reproducible from rng= too, for all three factories, not just MOEA/D's.
One caveat remains: crossover (classic variant only) and mutation are always your own operators,
constructed outside these factories, so their reproducibility is in your hands the same way it
already is for build_nsgaii()/build_smsemoa() -- pass them their own matching rng too.
The consequence: no execution-level equivalence test against the classic MOEAD is possible for
build_moead_de() the way test_nsgaii_equivalence.py/test_smsemoa_equivalence.py compare against
their classic counterparts (see Verified behavioral equivalence
below) -- two runs seeded "the same way" draw from genuinely different random number generators, so
they cannot produce identical fronts. What's verified instead: test_moead_replacement_structural_equivalence.py
checks that MOEADReplacement's replace/keep decisions exactly match the classic algorithm's
update_current_subproblem_neighborhood() given identical inputs (population, offspring, current
subproblem, scope) -- the logic that matters, isolated from randomness -- plus
test_moead_integration.py's hypervolume-floor checks for both variants (build_moead() has no
classic SBX-based counterpart in jMetalPy to compare against in the first place -- the existing
classic MOEAD class is already MOEA/D-DE despite its name, and always has been).
Reproducibility¶
No classic jMetalPy algorithm accepts a seed. EvolutionaryAlgorithm accepts an optional
rng: np.random.Generator, shared with any component that already declares a plain rng
attribute:
Every operator and Problem.create_solution() now accepts an optional explicit rng, falling back
to the global random/numpy.random state when none is given (the historical default, kept for
backward compatibility). Component-based algorithms built with an rng threaded through every
operator and create_solution() are fully reproducible from a single seed.
External archives¶
An external archive collects solutions independently of the population: every evaluated solution
is copied into it, regardless of what the population/replacement strategy keeps or discards. This
mirrors jMetal Java's SequentialEvaluationWithArchive + EvolutionaryAlgorithmWithArchive pair,
but as a plain constructor parameter rather than a subclass:
from jmetal.util.archive import CrowdingDistanceArchive
algorithm = build_nsgaii(
problem, population_size=100, offspring_population_size=100,
crossover=crossover, mutation=mutation,
archive=CrowdingDistanceArchive(maximum_size=100), # bounded
)
algorithm.run()
front = algorithm.result() # returns the archive's contents, not the final population
Any jmetal.util.archive.Archive works, bounded (CrowdingDistanceArchive, DistanceBasedArchive,
...) or unbounded (NonDominatedSolutionsArchive). The population still drives
selection/replacement as usual -- the archive is a pure addition, useful in particular for
multi-modal problems like ZDT4, where it guards against the population converging on a local
Pareto front. See examples/component/nsgaii_crowding_distance_archive_zdt4.py and
examples/component/nsgaii_unbounded_archive_dtlz2.py.
result()'s size. An unbounded archive can accumulate far more solutions than the population
size -- into the thousands over a full run. result() accounts for this: if the archive holds more
solutions than the population size, it reduces it to exactly that many via
distance_based_subset_selection_robust before returning, rather than handing back the whole
(potentially huge) archive. This mirrors jMetal Java's BestSolutionsArchive, which wraps an
otherwise-unbounded archive the same way. A bounded archive (CrowdingDistanceArchive, ...) never
exceeds the population size to begin with, so this is a no-op for it -- result() returns its
contents directly.
SequentialEvaluationWithArchive feeds the archive a whole generation at a time via
Archive.add_batch() rather than one solution at a time. For NonDominatedSolutionsArchive, whose
add() is O(n) per call, this replaces many individual insertions with a single
moocore.is_nondominated() call per generation -- on the DTLZ2 example above (an unbounded archive
growing into the thousands over 40000 evaluations), this took the run from ~43s to ~1.4s.
Discovering what's available: the catalogue reference¶
Before this architecture existed, answering "what can I configure in NSGA-II?" meant reading Java
source by hand. jmetal.component.catalogue_info.describe_catalogue() answers it from the running
code instead: which component slots exist, which implementations are available for each, and each
implementation's control parameters (name, type, default). It deliberately stops there -- no ranges
or distributions to explore those parameters, which is an automatic-configuration concern kept out
of scope for now.
from jmetal.component.catalogue_info import describe_catalogue
for slot, implementations in describe_catalogue().items():
print(slot)
for implementation in implementations:
print(" ", implementation.name, [p.name for p in implementation.parameters])
Everything below is derived from the real classes via inspect, not hand-maintained, so it cannot
drift out of sync with the code the way a separate parameter list could. Current output for the
NSGA-II catalogue:
SolutionsCreation¶
RandomSolutionsCreation
| Parameter | Type | Default |
|---|---|---|
problem |
Problem[~S] |
required |
number_of_solutions_to_create |
int |
required |
rng |
Generator \| None |
None |
Evaluation¶
SequentialEvaluation
| Parameter | Type | Default |
|---|---|---|
problem |
Problem[~S] |
required |
evaluator |
Optional[Evaluator[~S]] |
None |
Termination¶
TerminationByEvaluations
| Parameter | Type | Default |
|---|---|---|
max_evaluations |
int |
required |
Selection¶
TournamentSelection
| Parameter | Type | Default |
|---|---|---|
selection_operator |
TournamentSelection |
required |
mating_pool_size |
int |
required |
RandomSelection
| Parameter | Type | Default |
|---|---|---|
selection_operator |
RandomSelection |
required |
mating_pool_size |
int |
required |
MOEADSelection
| Parameter | Type | Default |
|---|---|---|
context |
MOEADContext |
required |
neighbourhood |
WeightVectorNeighborhood |
required |
number_of_parents |
int |
required |
selection_operator |
NaryRandomSolutionSelection \| None |
None |
Variation¶
CrossoverAndMutationVariation
| Parameter | Type | Default |
|---|---|---|
offspring_population_size |
int |
required |
crossover |
Crossover |
required |
mutation |
Mutation |
required |
DifferentialEvolutionCrossoverVariation
| Parameter | Type | Default |
|---|---|---|
context |
MOEADContext |
required |
crossover |
DifferentialEvolutionCrossover |
required |
mutation |
Mutation |
required |
Replacement¶
RankingAndDensityEstimatorReplacement
| Parameter | Type | Default |
|---|---|---|
ranking |
Ranking |
required |
density_estimator |
DensityEstimator |
required |
removal_policy |
RemovalPolicyType |
RemovalPolicyType.ONE_SHOT |
RankingAndCrowdingDistanceReplacement
| Parameter | Type | Default |
|---|---|---|
ranking |
Ranking |
None |
density_estimator |
DensityEstimator |
None |
SMSEMOAReplacement
| Parameter | Type | Default |
|---|---|---|
ranking |
Ranking |
None |
MOEADReplacement
| Parameter | Type | Default |
|---|---|---|
context |
MOEADContext |
required |
neighbourhood |
WeightVectorNeighborhood |
required |
aggregation_function |
AggregationFunction |
required |
max_number_of_replaced_solutions |
int |
required |
Crossover (jmetal.operator.crossover)¶
SBXCrossover
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
distribution_index |
float |
20.0 |
repair_operator |
Callable[[float, float, float], float] \| FloatRepairOperator \| None |
ClampFloatRepair() |
rng |
Generator \| None |
None |
IntegerSBXCrossover
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
distribution_index |
float |
20.0 |
rng |
Generator \| None |
None |
BLXAlphaCrossover
| Parameter | Type | Default |
|---|---|---|
probability |
float |
0.9 |
alpha |
float |
0.5 |
repair_operator |
Callable[[float, float, float], float] \| None |
None |
rng |
Generator \| None |
None |
BLXAlphaBetaCrossover
| Parameter | Type | Default |
|---|---|---|
probability |
float |
0.9 |
alpha |
float |
0.5 |
beta |
float |
0.5 |
repair_operator |
Callable[[float, float, float], float] \| None |
None |
rng |
Generator \| None |
None |
PMXCrossover
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
rng |
Generator \| None |
None |
CXCrossover
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
SPXCrossover
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
rng |
Generator \| None |
None |
DifferentialEvolutionCrossover
| Parameter | Type | Default |
|---|---|---|
CR |
float |
required |
F |
float |
required |
K |
float |
0.5 |
rng |
Generator \| None |
None |
Mutation (jmetal.operator.mutation)¶
PolynomialMutation
| Parameter | Type | Default |
|---|---|---|
probability |
float |
0.01 |
distribution_index |
float |
20.0 |
repair_operator |
Callable[[float, float, float], float] \| None |
None |
rng |
Generator \| None |
None |
IntegerPolynomialMutation
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
distribution_index |
float |
20.0 |
repair_operator |
Callable[[float, int, int], int] \| None |
None |
rng |
Generator \| None |
None |
BitFlipMutation
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
PermutationSwapMutation
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
rng |
Generator \| None |
None |
ScrambleMutation
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
rng |
Generator \| None |
None |
SimpleRandomMutation
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
rng |
Generator \| None |
None |
UniformMutation
| Parameter | Type | Default |
|---|---|---|
probability |
float |
required |
perturbation |
float |
0.5 |
repair_operator |
Callable[[float, float, float], float] \| None |
None |
rng |
Generator \| None |
None |
This table is generated from describe_catalogue()'s output and should be regenerated whenever
jmetal.component.catalogue_info.CATALOGUE changes (a new component lands, or a constructor
signature changes) -- it is not kept in sync automatically.
Verified behavioral equivalence¶
build_nsgaii(...)/build_smsemoa(...) and their classic NSGAII(...)/SMSEMOA(...) counterparts
produce identical final populations on ZDT1 and DTLZ2 given the same seed -- both reuse the exact
same operator classes and Problem.create_solution(), so identical random state produces identical
results. This is checked by dedicated tests
(tests/component/algorithm/multiobjective/test_nsgaii_equivalence.py,
test_smsemoa_equivalence.py) and is the acceptance criterion for this architecture: adopting
components changes how an algorithm is assembled, not what it computes.
MOEA/D is the exception: build_moead()/build_moead_de() deliberately draw all their own
randomness from a single rng, never the classic MOEAD's mix of global random/legacy
numpy.random/per-operator generators, so no seed makes their fronts bit-identical to the classic
algorithm's. See Building MOEA/D above for what's verified in its place.
Package layout¶
src/jmetal/component/
├── algorithm/
│ ├── algorithm_state.py # AlgorithmState
│ ├── evolutionary_algorithm.py # EvolutionaryAlgorithm
│ └── multiobjective/
│ ├── nsgaii.py # build_nsgaii()
│ ├── smsemoa.py # build_smsemoa()
│ └── moead.py # build_moead(), build_moead_de()
└── catalogue/
├── common/
│ ├── solutions_creation.py
│ ├── evaluation.py
│ └── termination.py
└── ea/
├── selection.py
├── variation.py
├── replacement.py
└── moead.py # MOEADContext and MOEA/D's Selection/Variation/Replacement
This covers NSGA-II, SMS-EMOA and MOEA/D -- the full scope of the component package for this release. Further MOEAs (SPEA2, MOCell) and a PSO template/catalogue are out of scope for now.