Skip to content

Utilities

jmetal.util provides the archives, distance metrics, comparators, density estimators, and other supporting utilities used throughout the framework. Evaluators, observers, and termination criteria have their own tutorials with usage examples — see Evaluators, Observers.

Archives

Archives are data structures for storing and managing collections of solutions during the optimization process. jMetalPy provides several implementations for different use cases:

  • Archive: Base abstract class for all archives
  • BoundedArchive: Archive with size limits
  • NonDominatedSolutionsArchive: Maintains only non-dominated solutions
  • CrowdingDistanceArchive: Uses crowding distance for diversity
  • DistanceBasedArchive: Adaptive distance-based selection

DistanceBasedArchive

DistanceBasedArchive provides adaptive selection strategies based on the number of objectives:

  • 2 objectives: Uses crowding distance selection for optimal diversity along Pareto fronts
  • >2 objectives: Uses distance-based subset selection with normalization

Key features:

  • Automatic strategy adaptation based on problem dimensionality
  • Robust normalization handling constant objectives
  • Support for custom distance measures
  • Non-dominated solution filtering using Pareto dominance
  • Memory-efficient in-place list modifications

Algorithm for many-objective problems:

  1. Normalize objectives to [0,1] range using min-max normalization
  2. Select random objective for initial sorting
  3. Choose extreme solutions (best and worst in random objective)
  4. Select remaining solutions using maximum minimum distance criterion

Example usage:

from jmetal.util.archive import DistanceBasedArchive
from jmetal.util.distance import DistanceMetric

# Create archive with a custom distance metric
archive = DistanceBasedArchive(
    maximum_size=10,
    metric=DistanceMetric.L2_SQUARED,
)

# Add solutions - automatically adapts strategy
for solution in solutions:
    archive.add(solution)

DistanceBasedArchive(maximum_size, metric=DistanceMetric.L2_SQUARED, weights=None, random_seed=None, dominance_comparator=None, use_vectorized=True)

Bases: BoundedArchive[S]

Archive that maintains solutions using adaptive distance-based subset selection.

This archive extends BoundedArchive to use a sophisticated selection mechanism: - For 2 objectives: Uses crowding distance selection - For >2 objectives: Uses robust distance-based subset selection with normalization

The implementation follows the Java jMetal SafeBestSolutionsArchive algorithm.

Initialize the distance-based archive.

Parameters:

Name Type Description Default
maximum_size int

Maximum number of solutions to maintain

required
metric DistanceMetric

Distance metric to use (default: L2_SQUARED)

L2_SQUARED
weights ndarray | None

Optional weights for TCHEBY_WEIGHTED metric

None
random_seed int | None

Optional seed for reproducible results

None
dominance_comparator Comparator | None

Comparator for dominance (default: DominanceComparator)

None
use_vectorized bool

Whether to use vectorized implementation (default: True)

True
Source code in src/jmetal/util/archive.py
def __init__(
    self,
    maximum_size: int,
    metric: DistanceMetric = DistanceMetric.L2_SQUARED,
    weights: np.ndarray | None = None,
    random_seed: int | None = None,
    dominance_comparator: Comparator | None = None,
    use_vectorized: bool = True,
):
    """
    Initialize the distance-based archive.

    Args:
        maximum_size: Maximum number of solutions to maintain
        metric: Distance metric to use (default: L2_SQUARED)
        weights: Optional weights for TCHEBY_WEIGHTED metric
        random_seed: Optional seed for reproducible results
        dominance_comparator: Comparator for dominance (default: DominanceComparator)
        use_vectorized: Whether to use vectorized implementation (default: True)
    """
    if dominance_comparator is None:
        dominance_comparator = DominanceComparator()

    # Initialize parent with dummy comparator and density estimator
    # We'll override the selection mechanism in our custom add method
    super().__init__(
        maximum_size=maximum_size,
        comparator=SolutionAttributeComparator("dummy", lowest_is_best=True),
        density_estimator=CrowdingDistanceDensityEstimator(),
        dominance_comparator=dominance_comparator,
    )

    self.metric = metric
    self.weights = weights
    self.random_seed = random_seed
    self.use_vectorized = use_vectorized
    # Persistent RNG: created once so the draw sequence advances across
    # successive prunes instead of being replayed identically on every add().
    self._rng = np.random.default_rng(random_seed)
    # Deprecation warning: non-vectorized path will be removed in future releases
    if not self.use_vectorized:
        logging.warning(
            "DistanceBasedArchive: use_vectorized=False is deprecated and will be removed in a future release. "
            "Prefer the vectorized implementation (use_vectorized=True)."
        )
        warnings.warn(
            "DistanceBasedArchive: use_vectorized=False is deprecated and will be removed in a future release.",
            DeprecationWarning,
        )

    # Thread safety for concurrent access
    self._lock = threading.Lock()
    # If requested, replace the non-dominated archive with the vectorized implementation.
    # We do this here (instead of changing BoundedArchive) to keep changes minimal
    # and to preserve backward-compatibility when use_vectorized=False.
    if self.use_vectorized:
        try:
            # Instantiate vectorized archive with the same dominance comparator
            self.non_dominated_solution_archive = VectorizedNonDominatedSolutionsArchive(
                dominance_comparator=dominance_comparator
            )
            # Keep the solution_list reference in sync as BoundedArchive expects
            self.solution_list = self.non_dominated_solution_archive.solution_list
        except Exception as _e:
            logging.warning(
                "DistanceBasedArchive: could not create VectorizedNonDominatedSolutionsArchive (%s). Falling back.",
                _e,
            )

add(solution)

Add a solution to the archive using non-dominated sorting and distance-based selection. Thread-safe implementation for concurrent use.

Parameters:

Name Type Description Default
solution S

Solution to add

required

Returns:

Type Description
bool

True if solution was added or archive was modified, False otherwise

Source code in src/jmetal/util/archive.py
def add(self, solution: S) -> bool:
    """
    Add a solution to the archive using non-dominated sorting and distance-based selection.
    Thread-safe implementation for concurrent use.

    Args:
        solution: Solution to add

    Returns:
        True if solution was added or archive was modified, False otherwise
    """
    with self._lock:
        # First, add to non-dominated archive (this handles dominance)
        success = self.non_dominated_solution_archive.add(solution)
        if success and self.size() > self.maximum_size:
            # Apply distance-based subset selection, reusing the persistent RNG
            # so its state advances across successive prunes.
            selected_solutions = distance_based_subset_selection_robust(
                self.solution_list,
                self.maximum_size,
                self.metric,
                self.weights,
                self.random_seed,
                self.use_vectorized,
                self._rng,
            )

            # Update solution list with selected solutions
            # IMPORTANT: Clear and extend to maintain reference from parent class
            self.solution_list.clear()
            self.solution_list.extend(selected_solutions)

        return success

compute_density_estimator()

Override parent method since we use distance-based selection instead. This method is called by parent class but we don't need density estimation.

Source code in src/jmetal/util/archive.py
def compute_density_estimator(self):
    """
    Override parent method since we use distance-based selection instead.
    This method is called by parent class but we don't need density estimation.
    """
    # Do nothing - we use distance-based selection instead of density estimation
    pass

Distance-based subset selection

Standalone function for distance-based subset selection that can be used independently of the archive.

  • Selection strategy: for 2 objectives, delegates to crowding distance selection; for more than 2, uses the distance-based algorithm with normalization.
from jmetal.util.archive import distance_based_subset_selection

# Select 5 best solutions from a larger set
selected = distance_based_subset_selection(
    solution_list=all_solutions,
    subset_size=5,
)

distance_based_subset_selection(solution_list, subset_size, distance_measure=None, metric=DistanceMetric.L2_SQUARED, weights=None, random_seed=None)

Backward compatibility wrapper for distance_based_subset_selection_robust.

Parameters:

Name Type Description Default
solution_list list[S]

List of solutions to select from

required
subset_size int

Number of solutions to select

required
distance_measure object | None

Deprecated parameter (ignored)

None
metric DistanceMetric

Distance metric to use

L2_SQUARED
weights ndarray | None

Optional weights for TCHEBY_WEIGHTED metric

None
random_seed int | None

Optional seed for reproducible results

None

Returns:

Type Description
list[S]

List of selected solutions

Source code in src/jmetal/util/archive.py
def distance_based_subset_selection(
    solution_list: list[S],
    subset_size: int,
    distance_measure: object | None = None,
    metric: DistanceMetric = DistanceMetric.L2_SQUARED,
    weights: np.ndarray | None = None,
    random_seed: int | None = None,
) -> list[S]:
    """
    Backward compatibility wrapper for distance_based_subset_selection_robust.

    Args:
        solution_list: List of solutions to select from
        subset_size: Number of solutions to select
        distance_measure: Deprecated parameter (ignored)
        metric: Distance metric to use
        weights: Optional weights for TCHEBY_WEIGHTED metric
        random_seed: Optional seed for reproducible results

    Returns:
        List of selected solutions
    """
    return distance_based_subset_selection_robust(
        solution_list, subset_size, metric, weights, random_seed
    )

Other archive classes

Archive()

Bases: Generic[S], ABC

Source code in src/jmetal/util/archive.py
def __init__(self):
    self.solution_list: list[S] = []

add_batch(solutions)

Add many solutions at once.

The default implementation just calls add() once per solution; it exists so callers that have a whole batch on hand (e.g. an Evaluation component adding a generation's worth of evaluated solutions) have one method to call regardless of archive type. Subclasses where checking the whole batch at once is meaningfully faster than one insertion at a time (see NonDominatedSolutionsArchive) should override it.

Parameters:

Name Type Description Default
solutions list[S]

The solutions to add.

required
Source code in src/jmetal/util/archive.py
def add_batch(self, solutions: list[S]) -> None:
    """Add many solutions at once.

    The default implementation just calls `add()` once per solution; it exists
    so callers that have a whole batch on hand (e.g. an `Evaluation` component
    adding a generation's worth of evaluated solutions) have one method to call
    regardless of archive type. Subclasses where checking the whole batch at
    once is meaningfully faster than one insertion at a time (see
    `NonDominatedSolutionsArchive`) should override it.

    Args:
        solutions: The solutions to add.
    """
    for solution in solutions:
        self.add(solution)

BoundedArchive(maximum_size, comparator=None, density_estimator=None, dominance_comparator=DominanceComparator())

Bases: Archive[S]

Source code in src/jmetal/util/archive.py
def __init__(
    self,
    maximum_size: int,
    comparator: Comparator[S] = None,
    density_estimator: DensityEstimator = None,
    dominance_comparator: Comparator[S] = DominanceComparator(),
):
    super().__init__()
    self.maximum_size = maximum_size
    self.comparator = comparator
    self.density_estimator = density_estimator
    self.non_dominated_solution_archive: NonDominatedSolutionsArchive[S] = (
        NonDominatedSolutionsArchive(dominance_comparator=dominance_comparator)
    )
    self.solution_list = self.non_dominated_solution_archive.solution_list

NonDominatedSolutionsArchive(dominance_comparator=DominanceComparator(), objective_tolerance=1e-10)

Bases: Archive[S]

Archive that maintains only non-dominated solutions using Pareto dominance.

This implementation efficiently manages a collection of solutions by: - Adding new non-dominated solutions - Removing solutions dominated by new ones - Preventing duplicate solutions based on objectives

Time Complexity: O(n) per insertion, where n is archive size Space Complexity: O(n) for storing solutions

Initialize the non-dominated solutions archive.

Parameters:

Name Type Description Default
dominance_comparator Comparator

Comparator to determine dominance relationships

DominanceComparator()
objective_tolerance float

Tolerance for comparing floating-point objectives

1e-10
Source code in src/jmetal/util/archive.py
def __init__(
    self,
    dominance_comparator: Comparator = DominanceComparator(),
    objective_tolerance: float = 1e-10,
):
    """
    Initialize the non-dominated solutions archive.

    Args:
        dominance_comparator: Comparator to determine dominance relationships
        objective_tolerance: Tolerance for comparing floating-point objectives
    """
    super().__init__()
    self.comparator = dominance_comparator
    self.objective_tolerance = objective_tolerance

add(solution)

Add a solution to the archive if it's non-dominated and not duplicate.

This method efficiently handles the archive by: 1. Checking if the new solution is dominated by existing ones 2. Removing existing solutions dominated by the new one 3. Preventing addition of duplicate solutions

Parameters:

Name Type Description Default
solution S

Solution to add to the archive

required

Returns:

Type Description
bool

True if solution was added, False if rejected (dominated or duplicate)

Time Complexity: O(n) where n is the number of solutions in archive

Source code in src/jmetal/util/archive.py
def add(self, solution: S) -> bool:
    """
    Add a solution to the archive if it's non-dominated and not duplicate.

    This method efficiently handles the archive by:
    1. Checking if the new solution is dominated by existing ones
    2. Removing existing solutions dominated by the new one
    3. Preventing addition of duplicate solutions

    Args:
        solution: Solution to add to the archive

    Returns:
        True if solution was added, False if rejected (dominated or duplicate)

    Time Complexity: O(n) where n is the number of solutions in archive
    """
    # Handle empty archive case
    if not self.solution_list:
        self.solution_list.append(solution)
        return True

    # Check dominance against all existing solutions
    remaining_solutions = []

    for current_solution in self.solution_list:
        dominance_flag = self.comparator.compare(solution, current_solution)

        if dominance_flag == 1:
            # New solution is dominated by current -> reject immediately
            return False
        elif dominance_flag == 0:
            # No dominance relationship -> check for duplicates
            if self._objectives_equal(solution, current_solution):
                # Duplicate found -> reject
                return False
            # Keep the current solution as it's not dominated
            remaining_solutions.append(current_solution)
        # dominance_flag == -1: current solution is dominated -> don't add to remaining

    # Update archive with non-dominated solutions and add new one
    # IMPORTANT: Modify list in-place to maintain references from BoundedArchive
    self.solution_list.clear()
    self.solution_list.extend(remaining_solutions)
    self.solution_list.append(solution)
    return True

add_batch(solutions)

Add many solutions at once, filtering the combined set in a single pass.

add() is O(n) per call (checks the new solution against every existing one), so adding a batch of k solutions one at a time costs O(k * n) -- the pattern SequentialEvaluationWithArchive used to follow, calling add() once per evaluated solution even though a whole generation's worth was available at once. This instead combines the archive's current contents with the batch and filters the result with moocore.is_nondominated in one C-backed call, which is much faster than even a single Python-level add() call once the archive has more than a handful of solutions. Only meaningful when using the default DominanceComparator, matching add()'s own accelerated path elsewhere in this module; a custom comparator falls back to the base class's one-at-a-time loop, since moocore.is_nondominated only knows Pareto dominance.

Parameters:

Name Type Description Default
solutions list[S]

The solutions to add.

required
Source code in src/jmetal/util/archive.py
def add_batch(self, solutions: list[S]) -> None:
    """Add many solutions at once, filtering the combined set in a single pass.

    `add()` is O(n) per call (checks the new solution against every existing
    one), so adding a batch of k solutions one at a time costs O(k * n) --
    the pattern `SequentialEvaluationWithArchive` used to follow, calling
    `add()` once per evaluated solution even though a whole generation's worth
    was available at once. This instead combines the archive's current
    contents with the batch and filters the result with
    `moocore.is_nondominated` in one C-backed call, which is much faster than
    even a single Python-level `add()` call once the archive has more than a
    handful of solutions. Only meaningful when using the default
    `DominanceComparator`, matching `add()`'s own accelerated path elsewhere in
    this module; a custom comparator falls back to the base class's
    one-at-a-time loop, since `moocore.is_nondominated` only knows Pareto
    dominance.

    Args:
        solutions: The solutions to add.
    """
    if not solutions:
        return

    if not isinstance(self.comparator, DominanceComparator):
        super().add_batch(solutions)
        return

    combined = self.solution_list + list(solutions)
    objectives = np.array([solution.objectives for solution in combined], dtype=float)
    keep = moocore.is_nondominated(objectives)

    # IMPORTANT: Modify list in-place to maintain references from BoundedArchive
    self.solution_list.clear()
    self.solution_list.extend(
        solution for solution, is_kept in zip(combined, keep, strict=True) if is_kept
    )

CrowdingDistanceArchive(maximum_size, dominance_comparator=DominanceComparator())

Bases: BoundedArchive[S]

Source code in src/jmetal/util/archive.py
def __init__(self, maximum_size: int, dominance_comparator=DominanceComparator()):
    super().__init__(
        maximum_size=maximum_size,
        comparator=SolutionAttributeComparator("crowding_distance", lowest_is_best=False),
        dominance_comparator=dominance_comparator,
        density_estimator=CrowdingDistanceDensityEstimator(),
    )

ArchiveWithReferencePoint(maximum_size, reference_point, comparator, density_estimator, rng=None)

Bases: BoundedArchive[S]

Source code in src/jmetal/util/archive.py
def __init__(
    self,
    maximum_size: int,
    reference_point: list[float],
    comparator: Comparator[S],
    density_estimator: DensityEstimator,
    rng: np.random.Generator | None = None,
):
    super().__init__(maximum_size, comparator, density_estimator)
    self.__reference_point = reference_point
    self.__comparator = comparator
    self.__density_estimator = density_estimator
    self.lock = Lock()
    self.rng = rng

Performance considerations

Time complexity:

  • DistanceBasedArchive.add(): O(n²) for >2 objectives, O(n log n) for 2 objectives
  • distance_based_subset_selection(): O(n²) worst case

Space complexity: O(n) for all archive implementations.

Scalability: Efficient for typical archive sizes (< 1000 solutions).

Recommendations:

  • Use CrowdingDistanceArchive for 2-objective problems requiring only crowding distance
  • Use DistanceBasedArchive for mixed or many-objective problems
  • Use NonDominatedSolutionsArchive when size limits are not needed

Distance metrics

The distance module provides various distance metrics and calculation utilities optimized for different use cases in multi-objective optimization:

  • DistanceMetric: Enumeration of available distance metrics — L2_SQUARED (squared Euclidean distance, fastest, avoids sqrt computation), LINF (L-infinity/Chebyshev distance, efficient for high dimensions), TCHEBY_WEIGHTED (weighted Chebyshev distance, supports preferences)
  • DistanceCalculator: High-performance static utility class for distance calculations
  • EuclideanDistance: Enhanced Euclidean distance with input validation for lists and arrays
  • CosineDistance: Cosine distance with reference point translation

Usage example:

import numpy as np
from jmetal.util.distance import DistanceCalculator, DistanceMetric

point1 = np.array([0.1, 0.5, 0.8])
point2 = np.array([0.3, 0.2, 0.9])

# L2 squared distance (fastest)
dist_l2 = DistanceCalculator.calculate_distance(
    point1, point2, DistanceMetric.L2_SQUARED
)

# Chebyshev distance
dist_linf = DistanceCalculator.calculate_distance(
    point1, point2, DistanceMetric.LINF
)

# Weighted Chebyshev distance
weights = np.array([0.5, 0.3, 0.2])
dist_weighted = DistanceCalculator.calculate_distance(
    point1, point2, DistanceMetric.TCHEBY_WEIGHTED, weights
)

distance

DistanceMetric

Bases: Enum

Enumeration of available distance metrics for optimized distance calculations.

Each metric is optimized for specific use cases: - L2_SQUARED: Fastest, avoids sqrt computation for relative distance comparisons - LINF: Efficient for high-dimensional spaces, emphasizes maximum difference - TCHEBY_WEIGHTED: Flexible weighted distance allowing preference specification

DistanceCalculator

High-performance distance calculator supporting multiple metrics.

This utility class provides optimized implementations for different distance metrics commonly used in multi-objective optimization. All methods are static for efficiency and support numpy array operations for vectorized performance.

calculate_distance(point1, point2, metric, weights=None) staticmethod

Calculate distance between two points using the specified metric.

Parameters:

Name Type Description Default
point1 ndarray

First point as numpy array

required
point2 ndarray

Second point as numpy array

required
metric DistanceMetric

Distance metric to use

required
weights ndarray | None

Optional weights for TCHEBY_WEIGHTED metric

None

Returns:

Name Type Description
float float

Distance between points according to specified metric

Raises:

Type Description
ValueError

If metric is invalid or weights are required but not provided

Source code in src/jmetal/util/distance.py
@staticmethod
def calculate_distance(
    point1: numpy.ndarray,
    point2: numpy.ndarray,
    metric: DistanceMetric,
    weights: numpy.ndarray | None = None,
) -> float:
    """
    Calculate distance between two points using the specified metric.

    Args:
        point1: First point as numpy array
        point2: Second point as numpy array
        metric: Distance metric to use
        weights: Optional weights for TCHEBY_WEIGHTED metric

    Returns:
        float: Distance between points according to specified metric

    Raises:
        ValueError: If metric is invalid or weights are required but not provided
    """
    if metric == DistanceMetric.L2_SQUARED:
        return DistanceCalculator._l2_squared(point1, point2)
    elif metric == DistanceMetric.LINF:
        return DistanceCalculator._linf(point1, point2)
    elif metric == DistanceMetric.TCHEBY_WEIGHTED:
        if weights is None:
            raise ValueError("Weights required for TCHEBY_WEIGHTED metric")
        return DistanceCalculator._tcheby_weighted(point1, point2, weights)
    else:
        raise ValueError(f"Unknown distance metric: {metric}")

calculate_distance_matrix(points, metric, weights=None) staticmethod

Calculate pairwise distance matrix between all points using vectorized operations.

This is significantly faster than calculating distances individually, especially for large numbers of points. Uses optimized NumPy operations for maximum performance.

Parameters:

Name Type Description Default
points ndarray

2D array where each row is a point (n_points, n_dimensions)

required
metric DistanceMetric

Distance metric to use

required
weights ndarray | None

Optional weights for TCHEBY_WEIGHTED metric

None

Returns:

Type Description
ndarray

numpy.ndarray: Symmetric distance matrix (n_points, n_points)

Raises:

Type Description
ValueError

If metric is invalid or weights are required but not provided

Source code in src/jmetal/util/distance.py
@staticmethod
def calculate_distance_matrix(
    points: numpy.ndarray, metric: DistanceMetric, weights: numpy.ndarray | None = None
) -> numpy.ndarray:
    """
    Calculate pairwise distance matrix between all points using vectorized operations.

    This is significantly faster than calculating distances individually,
    especially for large numbers of points. Uses optimized NumPy operations
    for maximum performance.

    Args:
        points: 2D array where each row is a point (n_points, n_dimensions)
        metric: Distance metric to use
        weights: Optional weights for TCHEBY_WEIGHTED metric

    Returns:
        numpy.ndarray: Symmetric distance matrix (n_points, n_points)

    Raises:
        ValueError: If metric is invalid or weights are required but not provided
    """
    n_points = points.shape[0]

    if metric == DistanceMetric.L2_SQUARED:
        return DistanceCalculator._l2_squared_matrix(points)
    elif metric == DistanceMetric.LINF:
        return DistanceCalculator._linf_matrix(points)
    elif metric == DistanceMetric.TCHEBY_WEIGHTED:
        if weights is None:
            raise ValueError("Weights required for TCHEBY_WEIGHTED metric")
        return DistanceCalculator._tcheby_weighted_matrix(points, weights)
    else:
        raise ValueError(f"Unknown distance metric: {metric}")

calculate_min_distances_vectorized(points, selected_indices, metric, weights=None) staticmethod

Calculate minimum distances from each point to the selected points using vectorized operations.

This is optimized for the subset selection process where we need to find the minimum distance from each candidate point to any already selected point.

Parameters:

Name Type Description Default
points ndarray

2D array of all points (n_points, n_dimensions)

required
selected_indices list[int]

List of indices of already selected points

required
metric DistanceMetric

Distance metric to use

required
weights ndarray | None

Optional weights for TCHEBY_WEIGHTED metric

None

Returns:

Type Description
ndarray

numpy.ndarray: Array of minimum distances for each point

Source code in src/jmetal/util/distance.py
@staticmethod
def calculate_min_distances_vectorized(
    points: numpy.ndarray,
    selected_indices: list[int],
    metric: DistanceMetric,
    weights: numpy.ndarray | None = None,
) -> numpy.ndarray:
    """
    Calculate minimum distances from each point to the selected points using vectorized operations.

    This is optimized for the subset selection process where we need to find
    the minimum distance from each candidate point to any already selected point.

    Args:
        points: 2D array of all points (n_points, n_dimensions)
        selected_indices: List of indices of already selected points
        metric: Distance metric to use
        weights: Optional weights for TCHEBY_WEIGHTED metric

    Returns:
        numpy.ndarray: Array of minimum distances for each point
    """
    if len(selected_indices) == 0:
        # If no points selected yet, return infinite distances
        return numpy.full(points.shape[0], numpy.inf)

    # Extract selected points
    selected_points = points[selected_indices]

    # Calculate minimum distances
    if metric == DistanceMetric.L2_SQUARED:
        min_distances = DistanceCalculator._min_l2_squared_distances(points, selected_points)
    elif metric == DistanceMetric.LINF:
        min_distances = DistanceCalculator._min_linf_distances(points, selected_points)
    elif metric == DistanceMetric.TCHEBY_WEIGHTED:
        if weights is None:
            raise ValueError("Weights required for TCHEBY_WEIGHTED metric")
        min_distances = DistanceCalculator._min_tcheby_weighted_distances(
            points, selected_points, weights
        )
    else:
        raise ValueError(f"Unknown distance metric: {metric}")

    # Set selected points to have infinite distance (they should not be reselected)
    for idx in selected_indices:
        min_distances[idx] = numpy.inf

    return min_distances

EuclideanDistance

Bases: Distance

Euclidean distance implementation with enhanced type safety and validation.

Supports both Python lists and numpy arrays as input. Provides comprehensive input validation for robust operation.

get_distance(list1, list2)

Calculate the Euclidean distance between two points.

Parameters:

Name Type Description Default
list1 list[float] | ndarray

First point as list or numpy array

required
list2 list[float] | ndarray

Second point as list or numpy array

required

Returns:

Name Type Description
float float

Euclidean distance between the points

Raises:

Type Description
ValueError

If inputs have different dimensions or are empty

TypeError

If inputs cannot be converted to numeric arrays

Source code in src/jmetal/util/distance.py
def get_distance(
    self, list1: list[float] | numpy.ndarray, list2: list[float] | numpy.ndarray
) -> float:
    """
    Calculate the Euclidean distance between two points.

    Args:
        list1: First point as list or numpy array
        list2: Second point as list or numpy array

    Returns:
        float: Euclidean distance between the points

    Raises:
        ValueError: If inputs have different dimensions or are empty
        TypeError: If inputs cannot be converted to numeric arrays
    """
    # Convert to numpy arrays for consistent handling
    try:
        arr1 = numpy.asarray(list1, dtype=float)
        arr2 = numpy.asarray(list2, dtype=float)
    except (ValueError, TypeError) as e:
        raise TypeError(f"Input vectors must be numeric: {e}")

    # Input validation - check for empty arrays
    if arr1.size == 0 or arr2.size == 0:
        raise ValueError("Input vectors cannot be empty")

    # Dimension validation
    if arr1.shape != arr2.shape:
        raise ValueError(
            f"Input vectors must have the same dimensions: {arr1.shape} vs {arr2.shape}"
        )

    # For 1D vectors, use scipy's optimized implementation
    if arr1.ndim == 1:
        return distance.euclidean(arr1, arr2)
    else:
        # For higher dimensions, flatten and compute
        return distance.euclidean(arr1.flatten(), arr2.flatten())

CosineDistance(reference_point)

Bases: Distance

Cosine distance implementation with reference point translation.

This class computes the cosine distance between two points after translating them relative to a reference point. The cosine distance is defined as: distance = 1 - cosine_similarity

Where cosine_similarity = (a·b) / (||a|| * ||b||)

The distance ranges from 0 (identical direction) to 2 (opposite directions).

Performance optimizations: - Cached reference point norm for efficiency - Vectorized numpy operations - Robust input validation and error handling

Initialize the cosine distance calculator with a reference point.

Parameters:

Name Type Description Default
reference_point list[float] | ndarray

Point used to translate input vectors before computing distance

required

Raises:

Type Description
ValueError

If reference point is empty

TypeError

If reference point is not numeric or is None

Source code in src/jmetal/util/distance.py
def __init__(self, reference_point: list[float] | numpy.ndarray):
    """
    Initialize the cosine distance calculator with a reference point.

    Args:
        reference_point: Point used to translate input vectors before computing distance

    Raises:
        ValueError: If reference point is empty
        TypeError: If reference point is not numeric or is None
    """
    if reference_point is None:
        raise TypeError("Reference point cannot be None")

    try:
        self.reference_point = numpy.asarray(reference_point, dtype=float)
    except (ValueError, TypeError) as e:
        raise TypeError(f"Reference point must be numeric: {e}")

    if self.reference_point.size == 0:
        raise ValueError("Reference point cannot be empty")

    # Cache reference point norm for efficiency
    self._ref_norm = numpy.linalg.norm(self.reference_point)

get_distance(list1, list2)

Calculate the cosine distance between two points relative to the reference point.

The computation follows these steps: 1. Translate both points by subtracting the reference point 2. Compute cosine similarity of the translated vectors 3. Return cosine distance = 1 - cosine_similarity

Parameters:

Name Type Description Default
list1 list[float] | ndarray

First point as list or numpy array

required
list2 list[float] | ndarray

Second point as list or numpy array

required

Returns:

Name Type Description
float float

Cosine distance in range [0, 2] 0 = vectors point in same direction 1 = vectors are orthogonal 2 = vectors point in opposite directions

Raises:

Type Description
ValueError

If inputs have different dimensions than reference point or are empty

TypeError

If inputs cannot be converted to numeric arrays

Source code in src/jmetal/util/distance.py
def get_distance(
    self, list1: list[float] | numpy.ndarray, list2: list[float] | numpy.ndarray
) -> float:
    """
    Calculate the cosine distance between two points relative to the reference point.

    The computation follows these steps:
    1. Translate both points by subtracting the reference point
    2. Compute cosine similarity of the translated vectors
    3. Return cosine distance = 1 - cosine_similarity

    Args:
        list1: First point as list or numpy array
        list2: Second point as list or numpy array

    Returns:
        float: Cosine distance in range [0, 2]
              0 = vectors point in same direction
              1 = vectors are orthogonal
              2 = vectors point in opposite directions

    Raises:
        ValueError: If inputs have different dimensions than reference point or are empty
        TypeError: If inputs cannot be converted to numeric arrays
    """
    # Convert inputs to numpy arrays
    try:
        vec1 = numpy.asarray(list1, dtype=float)
        vec2 = numpy.asarray(list2, dtype=float)
    except (ValueError, TypeError) as e:
        raise TypeError(f"Input vectors must be numeric: {e}")

    # Validate inputs
    if vec1.size == 0 or vec2.size == 0:
        raise ValueError("Input vectors cannot be empty")

    if vec1.shape != vec2.shape:
        raise ValueError(
            f"Input vectors must have same dimensions: {vec1.shape} vs {vec2.shape}"
        )

    if vec1.shape != self.reference_point.shape:
        raise ValueError(
            f"Input vectors must match reference point dimensions: "
            f"{vec1.shape} vs {self.reference_point.shape}"
        )

    # Translate vectors relative to reference point
    diff1 = vec1 - self.reference_point
    diff2 = vec2 - self.reference_point

    # Handle zero vectors after translation (return 0 since they're at reference point)
    norm1 = numpy.linalg.norm(diff1)
    norm2 = numpy.linalg.norm(diff2)

    if norm1 == 0.0 and norm2 == 0.0:
        # Both vectors are at the reference point
        return 0.0
    elif norm1 == 0.0 or norm2 == 0.0:
        # One vector is at reference point, other is not
        return 1.0

    # Compute cosine similarity
    dot_product = numpy.dot(diff1, diff2)
    cosine_similarity = dot_product / (norm1 * norm2)

    # Clamp to handle numerical precision issues
    cosine_similarity = numpy.clip(cosine_similarity, -1.0, 1.0)

    # Return cosine distance = 1 - cosine_similarity
    distance_result = 1.0 - cosine_similarity

    # Handle numerical precision for identical vectors (should be exactly 0)
    if numpy.allclose(diff1, diff2, rtol=1e-15, atol=1e-15):
        return 0.0

    return distance_result

get_similarity(list1, list2)

Calculate the cosine similarity between two points relative to the reference point.

This is a convenience method that returns the similarity instead of distance.

Parameters:

Name Type Description Default
list1 list[float] | ndarray

First point as list or numpy array

required
list2 list[float] | ndarray

Second point as list or numpy array

required

Returns:

Type Description
float

Cosine similarity in range [-1, 1]: 1 means the vectors point in the same direction,

float

0 means they are orthogonal, and -1 means they point in opposite directions.

Source code in src/jmetal/util/distance.py
def get_similarity(
    self, list1: list[float] | numpy.ndarray, list2: list[float] | numpy.ndarray
) -> float:
    """
    Calculate the cosine similarity between two points relative to the reference point.

    This is a convenience method that returns the similarity instead of distance.

    Args:
        list1: First point as list or numpy array
        list2: Second point as list or numpy array

    Returns:
        Cosine similarity in range [-1, 1]: 1 means the vectors point in the same direction,
        0 means they are orthogonal, and -1 means they point in opposite directions.
    """
    distance = self.get_distance(list1, list2)
    return 1.0 - distance

Comparators, rankings, and density estimators

comparator

MultiComparator(comparator_list)

Bases: Comparator

This comparator takes a list of comparators and check all of them iteratively until a value != 0 is obtained or the list becomes empty

Source code in src/jmetal/util/comparator.py
def __init__(self, comparator_list: [Comparator]):
    self.comparator_list: [Comparator] = comparator_list

ObjectiveComparator(objectiveId)

Bases: Comparator

Compares two solutions according to a particular objective

Source code in src/jmetal/util/comparator.py
def __init__(self, objectiveId):
    self.objectiveId = objectiveId

ranking

FastNonDominatedRanking(comparator=DominanceComparator())

Bases: Ranking[list[S]]

Class implementing the non-dominated ranking of NSGA-II proposed by Deb et al., see [Deb2002]_

Source code in src/jmetal/util/ranking.py
def __init__(self, comparator: Comparator = DominanceComparator()):
    super().__init__(comparator)

compute_ranking(solutions, k=None)

Compute ranking of solutions.

Optimized implementation with improved performance: - Early termination when k solutions found - Efficient front construction - Minimal object allocations

:param solutions: Solution list. :param k: Number of individuals.

Source code in src/jmetal/util/ranking.py
def compute_ranking(self, solutions: list[S], k: int = None):
    """Compute ranking of solutions.

    Optimized implementation with improved performance:
    - Early termination when k solutions found
    - Efficient front construction
    - Minimal object allocations

    :param solutions: Solution list.
    :param k: Number of individuals.
    """
    if not solutions:
        self.ranked_sublists = []
        return self.ranked_sublists

    num_solutions = len(solutions)

    # number of solutions dominating solution ith
    dominating_ith = [0] * num_solutions

    # list of solutions dominated by solution ith
    ith_dominated: list[list[int]] = [[] for _ in range(num_solutions)]

    # Try a vectorized dominance computation when using the default
    # DominanceComparator to avoid Python-level loops and many calls
    # to the comparator. Fallback to the original nested loops when
    # a custom comparator is provided or when solutions have
    # heterogeneous objective lengths.
    can_vectorize = isinstance(self.comparator, DominanceComparator)
    if can_vectorize:
        try:
            objectives = np.asarray([s.objectives for s in solutions], dtype=float)
            # pairwise comparisons: for minimization, i dominates j if
            # all(obj_i <= obj_j) and any(obj_i < obj_j)
            le = np.all(objectives[:, None, :] <= objectives[None, :, :], axis=2)
            lt = np.any(objectives[:, None, :] < objectives[None, :, :], axis=2)
            i_dom_j = le & lt
            # ensure diagonal is False
            np.fill_diagonal(i_dom_j, False)

            # dominating_ith[j] = number of solutions that dominate j
            dominating_counts = np.sum(i_dom_j, axis=0)
            for idx in range(num_solutions):
                dominating_ith[idx] = int(dominating_counts[idx])

            # ith_dominated[i] = list of indices j dominated by i
            for i in range(num_solutions):
                ith_dominated[i] = list(np.nonzero(i_dom_j[i])[0].tolist())

            # number of pairwise comparisons (unique unordered pairs)
            self.number_of_comparisons = num_solutions * (num_solutions - 1) // 2
        except Exception:
            # If any unexpected issue arises (e.g., non-numeric objectives),
            # fall back to the safe python implementation.
            can_vectorize = False

    if not can_vectorize:
        # Optimized dominance comparison with early break (original)
        for p in range(num_solutions - 1):
            for q in range(p + 1, num_solutions):
                dominance_test_result = self.comparator.compare(solutions[p], solutions[q])
                self.number_of_comparisons += 1

                if dominance_test_result == -1:
                    ith_dominated[p].append(q)
                    dominating_ith[q] += 1
                elif dominance_test_result == 1:
                    ith_dominated[q].append(p)
                    dominating_ith[p] += 1

    # Initialize first front efficiently
    current_front = []
    for i in range(num_solutions):
        if dominating_ith[i] == 0:
            current_front.append(i)
            solutions[i].attributes["dominance_ranking"] = 0

    # Build ranked sublists incrementally with early termination
    self.ranked_sublists = []
    front_index = 0
    total_count = 0

    while current_front:
        # Convert indices to solutions efficiently
        front_solutions = [solutions[idx] for idx in current_front]
        self.ranked_sublists.append(front_solutions)

        # Early termination check
        total_count += len(current_front)
        if k and total_count >= k:
            break

        # Prepare next front
        next_front = []
        for p in current_front:
            for q in ith_dominated[p]:
                dominating_ith[q] -= 1
                if dominating_ith[q] == 0:
                    next_front.append(q)
                    solutions[q].attributes["dominance_ranking"] = front_index + 1

        current_front = next_front
        front_index += 1

    # Truncate if k specified
    if k and total_count > k:
        count = 0
        for i, front in enumerate(self.ranked_sublists):
            count += len(front)
            if count >= k:
                self.ranked_sublists = self.ranked_sublists[: i + 1]
                break

    return self.ranked_sublists

StrengthRanking(comparator=DominanceComparator())

Bases: Ranking[list[S]]

Class implementing a ranking scheme based on the strength ranking used in SPEA2.

Source code in src/jmetal/util/ranking.py
def __init__(self, comparator: Comparator = DominanceComparator()):
    super().__init__(comparator)

compute_ranking(solutions, k=None)

Compute ranking of solutions using the provided dominance comparator.

:param solutions: Solution list. :param k: Number of individuals.

Source code in src/jmetal/util/ranking.py
def compute_ranking(self, solutions: list[S], k: int = None):
    """
    Compute ranking of solutions using the provided dominance comparator.

    :param solutions: Solution list.
    :param k: Number of individuals.
    """
    if not solutions:
        self.ranked_sublists = []
        return self.ranked_sublists

    n = len(solutions)
    strength = [0] * n
    raw_fitness = [0] * n

    # Compute strength values (number of solutions each solution dominates)
    for i in range(n):
        for j in range(n):
            if i == j:
                continue
            # Use the provided comparator to check if solution i dominates solution j
            if self.comparator.compare(solutions[i], solutions[j]) < 0:
                strength[i] += 1

    # Compute raw fitness (sum of strengths of dominators)
    for i in range(n):
        for j in range(n):
            if i == j:
                continue
            # Check if solution j dominates solution i
            if self.comparator.compare(solutions[j], solutions[i]) < 0:
                raw_fitness[i] += strength[j]

    # Store raw fitness in the strength_ranking attribute and find max fitness
    max_fitness = 0
    for i in range(n):
        fitness = int(raw_fitness[i])
        solutions[i].attributes["strength_ranking"] = fitness
        if fitness > max_fitness:
            max_fitness = fitness

    # Group solutions by raw fitness (ascending order)
    fitness_to_solutions: dict[int, list[S]] = {}
    for i, fit in enumerate(raw_fitness):
        if fit not in fitness_to_solutions:
            fitness_to_solutions[fit] = []
        fitness_to_solutions[fit].append(solutions[i])

    # Create ranked sublists sorted by fitness (ascending order)
    self.ranked_sublists = [fitness_to_solutions[f] for f in sorted(fitness_to_solutions)]

    return self.ranked_sublists

density_estimator

S = TypeVar('S') module-attribute

.. module:: density_estimator :platform: Unix, Windows :synopsis: Module including the implementation of density estimators.

.. moduleauthor:: Antonio J. Nebro ajnebro@uma.es

DensityEstimator

Bases: list[S], ABC

This is the interface of any density estimator algorithm.

CrowdingDistanceDensityEstimator

Bases: DensityEstimator[list[S]]

This class implements a DensityEstimator based on the crowding distance of algorithm NSGA-II.

compute_density_estimator(front)

This function performs the computation of the crowding density estimation over the solution list.

.. note:: This method assign the distance in the inner elements of the solution list.

:param front: The list of solutions.

Source code in src/jmetal/util/density_estimator.py
def compute_density_estimator(self, front: list[S]):
    """This function performs the computation of the crowding density estimation over the solution list.

    .. note::
       This method assign the distance in the inner elements of the solution list.

    :param front: The list of solutions.
    """
    size = len(front)

    if size == 0:
        return
    elif size == 1:
        front[0].attributes["crowding_distance"] = float("inf")
        return
    elif size == 2:
        front[0].attributes["crowding_distance"] = float("inf")
        front[1].attributes["crowding_distance"] = float("inf")
        return

    for i in range(len(front)):
        front[i].attributes["crowding_distance"] = 0.0

    number_of_objectives = len(front[0].objectives)

    for i in range(number_of_objectives):
        # Sort the population by Obj n
        front = sorted(front, key=lambda x: x.objectives[i])
        objective_minn = front[0].objectives[i]
        objective_maxn = front[len(front) - 1].objectives[i]

        # Set de crowding distance
        front[0].attributes["crowding_distance"] = float("inf")
        front[size - 1].attributes["crowding_distance"] = float("inf")

        for j in range(1, size - 1):
            distance = front[j + 1].objectives[i] - front[j - 1].objectives[i]

            # Check if minimum and maximum are the same (in which case do nothing)
            if objective_maxn - objective_minn == 0:
                pass
                # logger.warning('Minimum and maximum are the same!')
            else:
                distance = distance / (objective_maxn - objective_minn)

            distance += front[j].attributes["crowding_distance"]
            front[j].attributes["crowding_distance"] = distance

KNearestNeighborDensityEstimator(k=1)

Bases: DensityEstimator[list[S]]

This class implements a density estimator based on the distance to the k-th nearest solution.

Source code in src/jmetal/util/density_estimator.py
def __init__(self, k: int = 1):
    super().__init__()
    self.k = k
    self.distance_matrix: np.ndarray = np.empty((0, 0))

sort(solutions)

Sort solutions by knn_density (highest first).

Source code in src/jmetal/util/density_estimator.py
def sort(self, solutions: list[S]) -> list[S]:
    """
    Sort solutions by knn_density (highest first).
    """
    solutions.sort(key=lambda s: s.attributes.get("knn_density", float("inf")), reverse=True)

HypervolumeContributionDensityEstimator(reference_point=None)

Bases: DensityEstimator[list[S]]

Density estimator based on the hypervolume contribution of each solution.

Source code in src/jmetal/util/density_estimator.py
def __init__(self, reference_point=None):
    super().__init__()
    if reference_point is None:
        raise ValueError("reference_point for hypervolume contribution cannot be None.")
    if isinstance(reference_point, list | tuple | numpy.ndarray) and len(reference_point) == 0:
        raise ValueError("reference_point for hypervolume contribution cannot be empty.")
    self.reference_point = reference_point

compute_density_estimator(solutions)

Computes the hypervolume contribution for each solution in the list. Stores the value in solution.attributes["hv_contribution"].

Source code in src/jmetal/util/density_estimator.py
def compute_density_estimator(self, solutions: list[S]):
    """
    Computes the hypervolume contribution for each solution in the list.
    Stores the value in solution.attributes["hv_contribution"].
    """
    if not solutions:
        return

    # Extract objective values from solutions
    objectives = [solution.objectives for solution in solutions]

    # Compute contributions
    contributions = hv_contributions(objectives, ref=self.reference_point)

    # Assign contribution to each solution
    for sol, hv in zip(solutions, contributions):
        sol.attributes["hv_contribution"] = hv

sort(solutions)

Sorts solutions by their hypervolume contribution (highest first).

Source code in src/jmetal/util/density_estimator.py
def sort(self, solutions: list[S]) -> list[S]:
    """
    Sorts solutions by their hypervolume contribution (highest first).
    """
    solutions.sort(
        key=lambda s: s.attributes.get("hv_contribution", float("-inf")), reverse=True
    )

get_comparator() classmethod

Returns a comparator for the "hv_contribution" attribute.

Source code in src/jmetal/util/density_estimator.py
@classmethod
def get_comparator(cls) -> Comparator:
    """
    Returns a comparator for the "hv_contribution" attribute.
    """
    return SolutionAttributeComparator("hv_contribution", lowest_is_best=False)

Solutions, generators, and constraint handling

solution

logger = logging.getLogger(__name__) module-attribute

.. module:: solutions :platform: Unix, Windows :synopsis: Utils to print solutions.

.. moduleauthor:: Antonio J. Nebro ajnebro@uma.es, Antonio Benítez-Hidalgo antonio.b@uma.es

get_non_dominated_solutions(solutions)

Filter a list of solutions down to its non-dominated subset.

Delegates to moocore.is_nondominated for efficiency: the previous implementation added solutions one at a time to a NonDominatedSolutionsArchive, which is O(n) per insertion (O(n^2) overall) -- noticeably slow for the thousands of solutions an unbounded external archive can accumulate. moocore.is_nondominated filters the whole batch at once and already treats solutions with identical objectives as duplicates, keeping only the first occurrence -- the same behavior NonDominatedSolutionsArchive had.

Parameters:

Name Type Description Default
solutions list[Solution]

The solutions to filter.

required

Returns:

Type Description
list[Solution]

The non-dominated subset, in their original relative order.

Source code in src/jmetal/util/solution.py
def get_non_dominated_solutions(solutions: list[Solution]) -> list[Solution]:
    """Filter a list of solutions down to its non-dominated subset.

    Delegates to `moocore.is_nondominated` for efficiency: the previous
    implementation added solutions one at a time to a `NonDominatedSolutionsArchive`,
    which is O(n) per insertion (O(n^2) overall) -- noticeably slow for the
    thousands of solutions an unbounded external archive can accumulate.
    `moocore.is_nondominated` filters the whole batch at once and already treats
    solutions with identical objectives as duplicates, keeping only the first
    occurrence -- the same behavior `NonDominatedSolutionsArchive` had.

    Args:
        solutions: The solutions to filter.

    Returns:
        The non-dominated subset, in their original relative order.
    """
    if not solutions:
        return []

    objectives = np.array([solution.objectives for solution in solutions], dtype=float)
    keep = moocore.is_nondominated(objectives)

    return [solution for solution, is_kept in zip(solutions, keep, strict=True) if is_kept]

read_solutions(filename)

Reads a reference front from a file.

:param filename: File path where the front is located.

Source code in src/jmetal/util/solution.py
def read_solutions(filename: str) -> list[FloatSolution]:
    """Reads a reference front from a file.

    :param filename: File path where the front is located.
    """
    front = []

    if Path(filename).is_file():
        with open(filename) as file:
            for line in file:
                vector = [float(x) for x in line.split()]

                solution = FloatSolution([], [], len(vector))
                solution.objectives = vector

                front.append(solution)
    else:
        logger.warning(f"Reference front file was not found at {filename}")

    return front

generator

R = TypeVar('R') module-attribute

.. module:: generator :platform: Unix, Windows :synopsis: Population generators implementation.

.. moduleauthor:: Antonio Benítez-Hidalgo antonio.b@uma.es

constraint_handling

is_feasible(solution)

Returns a boolean value concerning the feasibility of a solution :param solution: :return: true if the solution is feasible; false otherwise

Source code in src/jmetal/util/constraint_handling.py
def is_feasible(solution: Solution) -> bool:
    """
    Returns a boolean value concerning the feasibility of a solution
    :param solution:
    :return: true if the solution is feasible; false otherwise
    """
    return number_of_violated_constraints(solution) == 0

number_of_violated_constraints(solution)

Returns the number of violated constraints of a solution :param solution: :return:

Source code in src/jmetal/util/constraint_handling.py
def number_of_violated_constraints(solution: Solution) -> int:
    """
    Returns the number of violated constraints of a solution
    :param solution:
    :return:
    """
    return sum([1 for _ in solution.constraints if _ < 0])

overall_constraint_violation_degree(solution)

Returns the constraint violation degree of a solution, which is the sum of the constraint values that are not zero :param solution: :return:

Source code in src/jmetal/util/constraint_handling.py
def overall_constraint_violation_degree(solution: Solution) -> float:
    """
    Returns the constraint violation degree of a solution, which is the sum of the constraint values that are not zero
    :param solution:
    :return:
    """
    return sum([value for value in solution.constraints if value < 0])

feasibility_ratio(solutions)

Returns the percentage of feasible solutions in a solution list :param solutions: :return:

Source code in src/jmetal/util/constraint_handling.py
def feasibility_ratio(solutions: list[Solution]):
    """
    Returns the percentage of feasible solutions in a solution list
    :param solutions:
    :return:
    """
    Check.that(len(solutions) > 0, "The solution list is empty")

    return sum(1 for solution in solutions if is_feasible(solution)) / len(solutions)

Aggregation and neighborhoods (MOEA/D)

aggregation_function

neighborhood

WeightVectorNeighborhood(number_of_weight_vectors, neighborhood_size, weight_vector_size=2, weights_path=None)

Bases: WeightNeighborhood

Source code in src/jmetal/util/neighborhood.py
def __init__(
    self,
    number_of_weight_vectors: int,
    neighborhood_size: int,
    weight_vector_size: int = 2,
    weights_path: str = None,
):
    super().__init__(
        number_of_weight_vectors, neighborhood_size, weight_vector_size, weights_path
    )
    self.__initialize_uniform_weight(weight_vector_size, number_of_weight_vectors)
    self.__initialize_neighborhood()

TwoDimensionalMesh(rows, columns, neighborhood)

Bases: Neighborhood

Class defining a bi-mensional mesh.

Source code in src/jmetal/util/neighborhood.py
def __init__(self, rows: int, columns: int, neighborhood: [[]]):
    self.rows = rows
    self.columns = columns
    self.neighborhood = neighborhood
    self.mesh = None
    self.__create_mesh()

C9(rows, columns)

Bases: TwoDimensionalMesh

Class defining an C9 neighborhood of a solution belonging to a list of solutions which is structured as a bi-dimensional mesh. The neighbors are those solutions that are in 1-hop distance

Shape
  • o *

Topology

north = {-1, 0} south = { 1 , 0} east = { 0 , 1} west = { 0 ,-1} north_east = {-1, 1} north_west = {-1, -1} south_east = { 1 , 1} south_west = { 1 ,-1}

Source code in src/jmetal/util/neighborhood.py
def __init__(self, rows: int, columns: int):
    super().__init__(
        rows, columns, [[-1, 0], [1, 0], [0, 1], [0, -1], [-1, 1], [-1, -1], [1, 1], [1, -1]]
    )

L5(rows, columns)

Bases: TwoDimensionalMesh

L5 neighborhood. Shape: * * o * *

Topology

north = -1, 0 south = 1, 0 east = 0, 1 west = 0, -1

Source code in src/jmetal/util/neighborhood.py
def __init__(self, rows: int, columns: int):
    super().__init__(rows, columns, [[-1, 0], [1, 0], [0, 1], [0, -1]])

Normalization, points, and checks

normalization

Normalization utilities for Pareto fronts.

This module provides functions to normalize Pareto fronts before applying quality indicators. Normalization is essential to avoid bias from objectives with different scales.

normalize_fronts(front, reference_front, method='reference_only')

Normalize both solution front and reference front to the same scale.

This ensures that quality indicators are not biased by objectives with different scales. Typically normalizes to [0,1] for each objective.

Parameters:

Name Type Description Default
front ndarray

Solution front matrix (each row is a solution, each column an objective).

required
reference_front ndarray

Reference front matrix (each row is a solution, each column an objective).

required
method NormalizationMethod

Normalization method. Options: - "minmax": Normalize to [0,1] using global min/max across both fronts - "zscore": Standardize using global mean and standard deviation - "reference_only": Normalize using only reference front bounds (recommended, default)

'reference_only'

Returns:

Type Description
tuple[ndarray, ndarray]

Tuple of (normalized_front, normalized_reference_front) where both matrices are normalized.

Raises:

Type Description
ValueError

If fronts have different number of objectives or invalid method.

Examples:

>>> front = np.array([[1.0, 100.0], [2.0, 200.0], [3.0, 150.0]])
>>> reference_front = np.array([[0.5, 80.0], [1.5, 120.0], [2.5, 180.0]])
>>> norm_front, norm_ref = normalize_fronts(front, reference_front)
>>> # Uses reference_only by default
>>> # Using different methods
>>> norm_front, norm_ref = normalize_fronts(front, reference_front, method="minmax")
Note

For quality indicators, "reference_only" is typically recommended as it uses the reference front to define the normalization bounds, which is more appropriate for performance assessment.

Source code in src/jmetal/util/normalization.py
def normalize_fronts(
    front: np.ndarray, reference_front: np.ndarray, method: NormalizationMethod = "reference_only"
) -> tuple[np.ndarray, np.ndarray]:
    """
    Normalize both solution front and reference front to the same scale.

    This ensures that quality indicators are not biased by objectives with different scales.
    Typically normalizes to [0,1] for each objective.

    Args:
        front: Solution front matrix (each row is a solution, each column an objective).
        reference_front: Reference front matrix (each row is a solution, each column an objective).
        method: Normalization method. Options:
            - "minmax": Normalize to [0,1] using global min/max across both fronts
            - "zscore": Standardize using global mean and standard deviation
            - "reference_only": Normalize using only reference front bounds (recommended, default)

    Returns:
        Tuple of (normalized_front, normalized_reference_front) where both matrices are normalized.

    Raises:
        ValueError: If fronts have different number of objectives or invalid method.

    Examples:
        >>> front = np.array([[1.0, 100.0], [2.0, 200.0], [3.0, 150.0]])
        >>> reference_front = np.array([[0.5, 80.0], [1.5, 120.0], [2.5, 180.0]])
        >>> norm_front, norm_ref = normalize_fronts(front, reference_front)
        >>> # Uses reference_only by default

        >>> # Using different methods
        >>> norm_front, norm_ref = normalize_fronts(front, reference_front, method="minmax")

    Note:
        For quality indicators, "reference_only" is typically recommended as it uses the
        reference front to define the normalization bounds, which is more appropriate
        for performance assessment.
    """
    if front.shape[1] != reference_front.shape[1]:
        raise ValueError("Fronts must have the same number of objectives")

    number_of_objectives = front.shape[1]
    normalized_front = np.zeros_like(front, dtype=np.float64)
    normalized_reference_front = np.zeros_like(reference_front, dtype=np.float64)

    if method == "minmax":
        # Use global min/max across both fronts
        combined_matrix = np.vstack([front, reference_front])

        for objective_index in range(number_of_objectives):
            objective_column = combined_matrix[:, objective_index]
            objective_min = np.min(objective_column)
            objective_max = np.max(objective_column)
            objective_range = objective_max - objective_min

            if objective_range > 0:
                # Normalize to [0,1]
                normalized_front[:, objective_index] = (
                    front[:, objective_index] - objective_min
                ) / objective_range
                normalized_reference_front[:, objective_index] = (
                    reference_front[:, objective_index] - objective_min
                ) / objective_range
            else:
                # All values are the same
                normalized_front[:, objective_index] = 0.0
                normalized_reference_front[:, objective_index] = 0.0

    elif method == "zscore":
        # Standardize using global mean and standard deviation
        combined_matrix = np.vstack([front, reference_front])

        for objective_index in range(number_of_objectives):
            objective_column = combined_matrix[:, objective_index]
            objective_mean = np.mean(objective_column)
            objective_std = np.std(objective_column, ddof=1)  # Sample standard deviation

            if objective_std > 0:
                normalized_front[:, objective_index] = (
                    front[:, objective_index] - objective_mean
                ) / objective_std
                normalized_reference_front[:, objective_index] = (
                    reference_front[:, objective_index] - objective_mean
                ) / objective_std
            else:
                # All values are the same
                normalized_front[:, objective_index] = 0.0
                normalized_reference_front[:, objective_index] = 0.0

    elif method == "reference_only":
        # Use only reference front to define normalization bounds (recommended for quality indicators)
        for objective_index in range(number_of_objectives):
            reference_column = reference_front[:, objective_index]
            reference_min = np.min(reference_column)
            reference_max = np.max(reference_column)
            reference_range = reference_max - reference_min

            if reference_range > 0:
                # Normalize using reference front bounds
                normalized_front[:, objective_index] = (
                    front[:, objective_index] - reference_min
                ) / reference_range
                normalized_reference_front[:, objective_index] = (
                    reference_front[:, objective_index] - reference_min
                ) / reference_range
            else:
                # All reference values are the same
                normalized_front[:, objective_index] = front[:, objective_index] - reference_min
                normalized_reference_front[:, objective_index] = 0.0

    else:
        raise ValueError(
            f"Unknown normalization method: {method}. Use 'minmax', 'zscore', or 'reference_only'"
        )

    return normalized_front, normalized_reference_front

normalize_front(front, bounds_min, bounds_max)

Normalize a front using predefined bounds for each objective.

Parameters:

Name Type Description Default
front ndarray

Front matrix to normalize (each row is a solution, each column an objective).

required
bounds_min ndarray

Minimum values for each objective.

required
bounds_max ndarray

Maximum values for each objective.

required

Returns:

Type Description
ndarray

Normalized front matrix.

Raises:

Type Description
ValueError

If dimensions don't match.

Examples:

>>> front = np.array([[1.0, 100.0], [2.0, 200.0]])
>>> min_bounds = np.array([0.0, 50.0])
>>> max_bounds = np.array([5.0, 250.0])
>>> normalized = normalize_front(front, min_bounds, max_bounds)
Source code in src/jmetal/util/normalization.py
def normalize_front(
    front: np.ndarray, bounds_min: np.ndarray, bounds_max: np.ndarray
) -> np.ndarray:
    """
    Normalize a front using predefined bounds for each objective.

    Args:
        front: Front matrix to normalize (each row is a solution, each column an objective).
        bounds_min: Minimum values for each objective.
        bounds_max: Maximum values for each objective.

    Returns:
        Normalized front matrix.

    Raises:
        ValueError: If dimensions don't match.

    Examples:
        >>> front = np.array([[1.0, 100.0], [2.0, 200.0]])
        >>> min_bounds = np.array([0.0, 50.0])
        >>> max_bounds = np.array([5.0, 250.0])
        >>> normalized = normalize_front(front, min_bounds, max_bounds)
    """
    if not (front.shape[1] == len(bounds_min) == len(bounds_max)):
        raise ValueError("Dimension mismatch between front and bounds")

    normalized_front = np.zeros_like(front, dtype=np.float64)
    number_of_objectives = front.shape[1]

    for objective_index in range(number_of_objectives):
        objective_range = bounds_max[objective_index] - bounds_min[objective_index]

        if objective_range > 0:
            normalized_front[:, objective_index] = (
                front[:, objective_index] - bounds_min[objective_index]
            ) / objective_range
        else:
            normalized_front[:, objective_index] = (
                front[:, objective_index] - bounds_min[objective_index]
            )

    return normalized_front

solutions_to_matrix(solutions)

Convert a list of jMetal Solution objects to a numpy matrix of objectives.

Parameters:

Name Type Description Default
solutions list[Solution]

List of Solution objects.

required

Returns:

Type Description
ndarray

Matrix where each row is a solution and each column is an objective.

Examples:

>>> # Assuming solutions is a list of Solution objects
>>> matrix = solutions_to_matrix(solutions)
>>> normalized_matrix, _ = normalize_fronts(matrix, reference_matrix)
Source code in src/jmetal/util/normalization.py
def solutions_to_matrix(solutions: list[Solution]) -> np.ndarray:
    """
    Convert a list of jMetal Solution objects to a numpy matrix of objectives.

    Args:
        solutions: List of Solution objects.

    Returns:
        Matrix where each row is a solution and each column is an objective.

    Examples:
        >>> # Assuming solutions is a list of Solution objects
        >>> matrix = solutions_to_matrix(solutions)
        >>> normalized_matrix, _ = normalize_fronts(matrix, reference_matrix)
    """
    if not solutions:
        return np.array([]).reshape(0, 0)

    objectives_matrix = np.array([solution.objectives for solution in solutions])
    return objectives_matrix

normalize_solution_fronts(solutions, reference_solutions, method='reference_only')

Normalize fronts from jMetal Solution objects.

Convenience function that converts Solution objects to matrices and normalizes them.

Parameters:

Name Type Description Default
solutions list[Solution]

List of solution objects.

required
reference_solutions list[Solution]

List of reference solution objects.

required
method NormalizationMethod

Normalization method.

'reference_only'

Returns:

Type Description
tuple[ndarray, ndarray]

Tuple of (normalized_front, normalized_reference_front) as numpy arrays.

Examples:

>>> # Assuming solutions and reference_solutions are lists of Solution objects
>>> norm_front, norm_ref = normalize_solution_fronts(solutions, reference_solutions)
>>> # Can now be used with quality indicators
Source code in src/jmetal/util/normalization.py
def normalize_solution_fronts(
    solutions: list[Solution],
    reference_solutions: list[Solution],
    method: NormalizationMethod = "reference_only",
) -> tuple[np.ndarray, np.ndarray]:
    """
    Normalize fronts from jMetal Solution objects.

    Convenience function that converts Solution objects to matrices and normalizes them.

    Args:
        solutions: List of solution objects.
        reference_solutions: List of reference solution objects.
        method: Normalization method.

    Returns:
        Tuple of (normalized_front, normalized_reference_front) as numpy arrays.

    Examples:
        >>> # Assuming solutions and reference_solutions are lists of Solution objects
        >>> norm_front, norm_ref = normalize_solution_fronts(solutions, reference_solutions)
        >>> # Can now be used with quality indicators
    """
    front_matrix = solutions_to_matrix(solutions)
    reference_matrix = solutions_to_matrix(reference_solutions)

    return normalize_fronts(front_matrix, reference_matrix, method)

get_ideal_and_nadir_points(front)

Get ideal and nadir points from a front.

Parameters:

Name Type Description Default
front ndarray

Front matrix (each row is a solution, each column an objective).

required

Returns:

Type Description
ndarray

Tuple of (ideal_point, nadir_point) where:

ndarray
  • ideal_point: Minimum value for each objective
tuple[ndarray, ndarray]
  • nadir_point: Maximum value for each objective

Examples:

>>> front = np.array([[1.0, 3.0], [2.0, 2.0], [3.0, 1.0]])
>>> ideal, nadir = get_ideal_and_nadir_points(front)
>>> # ideal = [1.0, 1.0], nadir = [3.0, 3.0]
Source code in src/jmetal/util/normalization.py
def get_ideal_and_nadir_points(front: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """
    Get ideal and nadir points from a front.

    Args:
        front: Front matrix (each row is a solution, each column an objective).

    Returns:
        Tuple of (ideal_point, nadir_point) where:
        - ideal_point: Minimum value for each objective
        - nadir_point: Maximum value for each objective

    Examples:
        >>> front = np.array([[1.0, 3.0], [2.0, 2.0], [3.0, 1.0]])
        >>> ideal, nadir = get_ideal_and_nadir_points(front)
        >>> # ideal = [1.0, 1.0], nadir = [3.0, 3.0]
    """
    if front.size == 0:
        return np.array([]), np.array([])

    ideal_point = np.min(front, axis=0)
    nadir_point = np.max(front, axis=0)

    return ideal_point, nadir_point

normalize_to_unit_hypercube(front, ideal_point=None, nadir_point=None)

Normalize a front to the unit hypercube [0,1]^m.

Parameters:

Name Type Description Default
front ndarray

Front matrix to normalize.

required
ideal_point ndarray

Ideal point (minimum values). If None, computed from front.

None
nadir_point ndarray

Nadir point (maximum values). If None, computed from front.

None

Returns:

Type Description
ndarray

Normalized front in [0,1]^m.

Examples:

>>> front = np.array([[1.0, 3.0], [2.0, 2.0], [3.0, 1.0]])
>>> normalized = normalize_to_unit_hypercube(front)
>>> # All values will be in [0,1]
Source code in src/jmetal/util/normalization.py
def normalize_to_unit_hypercube(
    front: np.ndarray, ideal_point: np.ndarray = None, nadir_point: np.ndarray = None
) -> np.ndarray:
    """
    Normalize a front to the unit hypercube [0,1]^m.

    Args:
        front: Front matrix to normalize.
        ideal_point: Ideal point (minimum values). If None, computed from front.
        nadir_point: Nadir point (maximum values). If None, computed from front.

    Returns:
        Normalized front in [0,1]^m.

    Examples:
        >>> front = np.array([[1.0, 3.0], [2.0, 2.0], [3.0, 1.0]])
        >>> normalized = normalize_to_unit_hypercube(front)
        >>> # All values will be in [0,1]
    """
    if front.size == 0:
        return front

    if ideal_point is None or nadir_point is None:
        computed_ideal, computed_nadir = get_ideal_and_nadir_points(front)
        if ideal_point is None:
            ideal_point = computed_ideal
        if nadir_point is None:
            nadir_point = computed_nadir

    return normalize_front(front, ideal_point, nadir_point)

point

ckecking

Observable

observable

LOGGER = logging.getLogger('jmetal') module-attribute

.. module:: observable :platform: Unix, Windows :synopsis: Implementation of observable entities (using delegation) .. moduleauthor:: Antonio J. Nebro antonio@lcc.uma.es

Standalone plotting

A separate, CLI/benchmark-oriented plotting utility from jmetal.lab.visualization (see Front visualization): reads a CSV of objective values and writes a static PNG plus an optional interactive Plotly HTML file.

plotting

save_plt_to_file(solutions, filename, out_dir='results', html_plotly=False)

Save a visualization of a solution front to a PNG file.

Parameters - solutions: iterable of solutions (objects with .objectives) or an array-like numeric front - filename: base filename (no extension) used to build output names - out_dir: directory where PNG/HTML will be written - html_plotly: if True and Plotly is available, write an interactive HTML parallel-coordinates page for multi-objective fronts

Returns the path to the generated PNG file.

Source code in src/jmetal/util/plotting.py
def save_plt_to_file(
    solutions: Iterable, filename: str, out_dir: str = "results", html_plotly: bool = False
) -> str:
    """Save a visualization of a solution front to a PNG file.

    Parameters
    - solutions: iterable of solutions (objects with `.objectives`) or an array-like numeric front
    - filename: base filename (no extension) used to build output names
    - out_dir: directory where PNG/HTML will be written
    - html_plotly: if True and Plotly is available, write an interactive HTML parallel-coordinates page for multi-objective fronts

    Returns the path to the generated PNG file.
    """
    try:
        os.makedirs(out_dir, exist_ok=True)
    except Exception:
        pass

    # Convert to numpy array: prefer reading .objectives when available
    try:
        arr = np.asarray([s.objectives for s in solutions], dtype=float)
    except Exception:
        try:
            arr = np.asarray(solutions, dtype=float)
        except Exception:
            raise ValueError("Could not parse solutions into numeric objectives")

    if arr.ndim == 1:
        if arr.size == 0:
            raise ValueError("Empty front provided")
        arr = arr.reshape(-1, 1)

    n_points, n_obj = arr.shape
    png_path = os.path.join(out_dir, f"{filename}_front.png")

    if n_obj == 2:
        plt.figure(figsize=(6, 4))
        plt.scatter(arr[:, 0], arr[:, 1], s=8, c="C0", alpha=0.8)
        plt.xlabel("f1")
        plt.ylabel("f2")
        plt.title(f"{filename} approximation front (2D)")
        plt.tight_layout()
        plt.savefig(png_path, dpi=200)
        plt.close()

        # Optional interactive Plotly output for 2D
        if html_plotly and _PLOTLY_AVAILABLE:
            try:
                import pandas as pd

                df = pd.DataFrame(arr, columns=[f"f{i + 1}" for i in range(n_obj)])
                figly = px.scatter(
                    df,
                    x=df.columns[0],
                    y=df.columns[1],
                    title=f"{filename} approximation front (2D)",
                )
                html_path = os.path.join(out_dir, f"{filename}_front.html")
                pyoff.plot(figly, filename=html_path, auto_open=False)
            except Exception:
                pass

    elif n_obj == 3:
        axis_labels = [f"f{i + 1}" for i in range(n_obj)]
        fig = plt.figure(figsize=(6, 6))
        ax = fig.add_subplot(111, projection="3d")
        ax.scatter(arr[:, 0], arr[:, 1], arr[:, 2], s=8, c="C0", alpha=0.8)
        ax.set_xlabel(axis_labels[0])
        ax.set_ylabel(axis_labels[1])
        ax.set_zlabel(axis_labels[2])
        ax.set_title(f"{filename} approximation front (3D)")
        plt.tight_layout()
        fig.savefig(png_path, dpi=200)
        plt.close(fig)

        # Optional interactive Plotly output for 3D
        if html_plotly and _PLOTLY_AVAILABLE:
            try:
                import pandas as pd

                df = pd.DataFrame(arr, columns=axis_labels)
                figly = px.scatter_3d(
                    df,
                    x=axis_labels[0],
                    y=axis_labels[1],
                    z=axis_labels[2],
                    title=f"{filename} approximation front (3D)",
                )
                figly.update_layout(
                    scene=dict(
                        xaxis_title=axis_labels[0],
                        yaxis_title=axis_labels[1],
                        zaxis_title=axis_labels[2],
                    )
                )
                html_path = os.path.join(out_dir, f"{filename}_front.html")
                pyoff.plot(figly, filename=html_path, auto_open=False)
            except Exception:
                pass

    else:
        # Parallel coordinates (matplotlib): small stacked plots
        fig, axes = plt.subplots(
            nrows=n_obj, ncols=1, figsize=(8, max(3, n_obj * 1.2)), sharex=True
        )
        if n_obj == 1:
            axes = [axes]
        for i in range(n_obj):
            ax = axes[i]
            ax.plot(arr[:, i], color="C0", alpha=0.6)
            ax.set_ylabel(f"f{i + 1}")
        axes[-1].set_xlabel("solution index")
        fig.suptitle(f"{filename} approximation front (parallel coordinates)")
        plt.tight_layout()
        plt.savefig(png_path, dpi=200)
        plt.close(fig)

        # Optional interactive Plotly output
        if html_plotly and _PLOTLY_AVAILABLE:
            try:
                import pandas as pd

                df = pd.DataFrame(arr, columns=[f"f{i + 1}" for i in range(n_obj)])
                figly = px.parallel_coordinates(df, color=df.columns[0])
                html_path = os.path.join(out_dir, f"{filename}_front.html")
                pyoff.plot(figly, filename=html_path, auto_open=False)
            except Exception:
                # Ignore Plotly/Pandas errors
                pass

    return png_path