Skip to content

Defining new problems

To include a problem in jMetalPy, it must implement the Problem interface from the jmetal.core.problem module.

Use case: Subset Sum

The goal is to find a subset S of W (list of non-negative integers) whose elements sum is closest to (without exceeding) C. For example, for the input \(W=\{3, 34, 4, 12, 5, 2\}\) and \(C=9\), one output could be \(S=\{4, 5\}\) (as it is a subset with sum 9).

In jMetalPy, this problem can be encoded as a binary problem with one objective (to be maximized) and one bit per element of W, indicating whether that element is selected:

import numpy as np

from jmetal.core.problem import BinaryProblem
from jmetal.core.solution import BinarySolution


class SubsetSum(BinaryProblem):
   def __init__(self, C: int, W: list):
      super().__init__()
      self.C = C
      self.W = np.array(W, dtype=float)

      self.number_of_bits = len(self.W)
      self.obj_directions = [self.MAXIMIZE]
      self.obj_labels = ['Sum']

   def number_of_variables(self) -> int:
      return self.number_of_bits

   def number_of_objectives(self) -> int:
      return 1

   def number_of_constraints(self) -> int:
      return 0

   def evaluate(self, solution: BinarySolution) -> BinarySolution:
      pass

   def create_solution(self) -> BinarySolution:
      pass

   def name(self) -> str:
      return 'Subset Sum'

Now we have to define the abstract methods evaluate and create_solution from the jmetal.core.problem.Problem class.

Note that each solution consists of one objective function to be maximized to be as close as possible to \(C\):

\[ \max{\sum_{i \in S}{s_i}} \]

Taking this into account, one solution could be created and evaluated as follows:

Note

jMetalPy assumes minimization by default. Therefore, we will have to negate the solution objective.

def evaluate(self, solution: BinarySolution) -> BinarySolution:
    selected_mask = solution.bits
    total_sum = np.sum(self.W[selected_mask])

    if total_sum > self.C:
        total_sum = self.C - (total_sum - self.C)
        if total_sum < 0.0:
            total_sum = 0.0

    solution.objectives[0] = -total_sum

    return solution

def create_solution(self) -> BinarySolution:
    solution = BinarySolution(
        number_of_variables=self.number_of_bits,
        number_of_objectives=self.number_of_objectives(),
    )
    solution.bits = np.random.choice([True, False], size=self.number_of_bits)

    return solution

BinarySolution stores its bits as a NumPy boolean array. Use the bits property to read or assign the whole array at once (as above), or the list-based variables property for element-by-element access.

Use case: Multi-objective Subset Sum

The former problem can be formulated as a multi-objective binary problem whose objectives are as follows:

  1. Maximize the sum of subsets to be as close as possible to \(C\) and
  2. Minimize the number of elements selected from \(W\).

This can be done by returning 2 from number_of_objectives, setting a second objective direction and label, and computing both objectives in evaluate:

 class SubsetSum(BinaryProblem):
    def __init__(self, C: int, W: list):
       super().__init__()
       self.C = C
       self.W = np.array(W, dtype=float)

       self.number_of_bits = len(self.W)
-      self.obj_directions = [self.MAXIMIZE]
-      self.obj_labels = ['Sum']
+      self.obj_directions = [self.MAXIMIZE, self.MINIMIZE]
+      self.obj_labels = ['Sum', 'No. of Objects']

    def number_of_variables(self) -> int:
       return self.number_of_bits

    def number_of_objectives(self) -> int:
-      return 1
+      return 2

    def number_of_constraints(self) -> int:
       return 0

    def evaluate(self, solution: BinarySolution) -> BinarySolution:
       selected_mask = solution.bits
       total_sum = np.sum(self.W[selected_mask])
+      number_of_objects = np.count_nonzero(selected_mask)

       if total_sum > self.C:
          total_sum = self.C - (total_sum - self.C)
          if total_sum < 0.0:
              total_sum = 0.0

       solution.objectives[0] = -total_sum
+      solution.objectives[1] = number_of_objects

       return solution

Both variants are available out of the box as jmetal.problem.singleobjective.unconstrained.SubsetSum and jmetal.problem.multiobjective.unconstrained.SubsetSum.

API

problem

This module defines the core problem interfaces for optimization in JMetalPy.

It provides abstract base classes for defining optimization problems of various types, including binary, float, integer, and permutation problems, as well as utilities for creating problems on the fly.

Problem()

Bases: Generic[S], ABC

Abstract base class for all optimization problems.

This class defines the common interface that all optimization problems must implement. It serves as the foundation for defining problems with different variable types (binary, float, integer, permutation) and characteristics (single/multi-objective, constrained/unconstrained).

Class constants

MINIMIZE: Constant indicating minimization of an objective. MAXIMIZE: Constant indicating maximization of an objective.

Initialize the problem with empty reference front, directions, and labels.

Source code in src/jmetal/core/problem.py
def __init__(self):
    """Initialize the problem with empty reference front, directions, and labels."""
    self.reference_front: list[S] = []  #: List of solutions representing the Pareto front.
    self.directions: list[int] = []  #: Optimization direction (minimize/maximize) per objective.
    self.labels: list[str] = []  #: Descriptive label per objective.

create_solution(rng=None) abstractmethod

Creates a random_search solution to the problem.

:param rng: Optional random generator for reproducible solution creation. When None, falls back to the global random/numpy.random state (the historical default), not a freshly created generator -- unlike the RNG-aware operators (SBXCrossover, etc.), which fall back to a new np.random.default_rng(). This asymmetry is deliberate: it keeps create_solution() with no arguments behaviorally identical to before this parameter existed, since random.seed()-based reproducibility (used throughout the classic algorithm hierarchy and the component-equivalence tests) depends on it. :return: Solution.

Source code in src/jmetal/core/problem.py
@abstractmethod
def create_solution(self, rng: np.random.Generator | None = None) -> S:
    """Creates a random_search solution to the problem.

    :param rng: Optional random generator for reproducible solution creation. When
        None, falls back to the global `random`/`numpy.random` state (the historical
        default), not a freshly created generator -- unlike the RNG-aware operators
        (`SBXCrossover`, etc.), which fall back to a new `np.random.default_rng()`.
        This asymmetry is deliberate: it keeps `create_solution()` with no arguments
        behaviorally identical to before this parameter existed, since
        `random.seed()`-based reproducibility (used throughout the classic algorithm
        hierarchy and the component-equivalence tests) depends on it.
    :return: Solution."""
    pass

evaluate(solution) abstractmethod

Evaluate a solution. For any new problem inheriting from :class:Problem, this method should be replaced. Note that this framework ASSUMES minimization, thus solutions must be evaluated in consequence.

:return: Evaluated solution.

Source code in src/jmetal/core/problem.py
@abstractmethod
def evaluate(self, solution: S) -> S:
    """Evaluate a solution. For any new problem inheriting from :class:`Problem`, this method should be replaced.
    Note that this framework ASSUMES minimization, thus solutions must be evaluated in consequence.

    :return: Evaluated solution."""
    pass

DynamicProblem()

Bases: Problem[S], Observer, ABC

Abstract base class for dynamic optimization problems.

Dynamic problems are those where the fitness landscape, constraints, or other characteristics may change over time. This class extends the base Problem interface with methods to detect and handle such changes.

This class also implements the Observer pattern to allow the problem to be notified of changes in the environment or other components.

The type parameter S represents the type of the solution this problem works with.

Source code in src/jmetal/core/problem.py
def __init__(self):
    """Initialize the problem with empty reference front, directions, and labels."""
    self.reference_front: list[S] = []  #: List of solutions representing the Pareto front.
    self.directions: list[int] = []  #: Optimization direction (minimize/maximize) per objective.
    self.labels: list[str] = []  #: Descriptive label per objective.

the_problem_has_changed() abstractmethod

Check if the problem has changed since the last check.

Returns:

Name Type Description
bool bool

True if the problem has changed, False otherwise.

Source code in src/jmetal/core/problem.py
@abstractmethod
def the_problem_has_changed(self) -> bool:
    """Check if the problem has changed since the last check.

    Returns:
        bool: True if the problem has changed, False otherwise.
    """
    pass

clear_changed() abstractmethod

Clear the changed flag after handling a change event.

This method should be called after the algorithm has responded to a change in the problem to reset the change detection mechanism.

Source code in src/jmetal/core/problem.py
@abstractmethod
def clear_changed(self) -> None:
    """Clear the changed flag after handling a change event.

    This method should be called after the algorithm has responded to a change
    in the problem to reset the change detection mechanism.
    """
    pass

BinaryProblem()

Bases: Problem[BinarySolution], ABC

Abstract base class for binary-encoded optimization problems.

This class is designed for problems where solutions are represented as bit strings. Each variable in the problem is encoded using a fixed number of bits, which can vary between variables.

Attributes:

Name Type Description
number_of_bits_per_variable

List specifying the number of bits used to encode each variable.

Initialize a binary problem with an empty list of bits per variable.

Source code in src/jmetal/core/problem.py
def __init__(self):
    """Initialize a binary problem with an empty list of bits per variable."""
    super().__init__()
    self.number_of_bits_per_variable = []

FloatProblem()

Bases: Problem[FloatSolution], ABC

Abstract base class for continuous optimization problems with float variables.

This class is designed for problems where decision variables can take any real value within specified lower and upper bounds. It's suitable for continuous optimization problems in any number of dimensions.

Attributes:

Name Type Description
lower_bound

List of lower bounds for each decision variable.

upper_bound

List of upper bounds for each decision variable.

Initialize a float problem with empty bounds.

Source code in src/jmetal/core/problem.py
def __init__(self):
    """Initialize a float problem with empty bounds."""
    super().__init__()
    self.lower_bound = []
    self.upper_bound = []

IntegerProblem()

Bases: Problem[IntegerSolution], ABC

Abstract base class for integer-constrained optimization problems.

This class is designed for problems where decision variables must take integer values within specified lower and upper bounds. It's suitable for discrete optimization problems, combinatorial problems, and mixed-integer problems.

Attributes:

Name Type Description
lower_bound

List of lower bounds (inclusive) for each decision variable.

upper_bound

List of upper bounds (inclusive) for each decision variable.

Initialize an integer problem with empty bounds.

Source code in src/jmetal/core/problem.py
def __init__(self):
    """Initialize an integer problem with empty bounds."""
    super().__init__()
    self.lower_bound = []
    self.upper_bound = []

PermutationProblem()

Bases: Problem[PermutationSolution], ABC

Abstract base class for permutation-based optimization problems.

This class is designed for problems where solutions are represented as permutations of a set of elements. Common applications include routing problems (like TSP), scheduling problems, and other combinatorial optimization problems where the order of elements is significant.

The permutation is represented as a list of integers from 0 to n-1, where n is the number of elements in the permutation.

Initialize a permutation problem.

Source code in src/jmetal/core/problem.py
def __init__(self):
    """Initialize a permutation problem."""
    super().__init__()

OnTheFlyFloatProblem()

Bases: FloatProblem

A utility class for defining float optimization problems dynamically at runtime.

This class allows users to define optimization problems programmatically by specifying the problem's variables, objectives, and constraints through method chaining. It's particularly useful for quick prototyping and testing.

Example

.. code-block:: python

# Define the problem's objective functions and constraints
def f1(x: List[float]) -> float:
    return 2.0 + (x[0] - 2.0)**2 + (x[1] - 1.0)**2

def f2(x: List[float]) -> float:
    return 9.0 * x[0] - (x[1] - 1.0)**2

def c1(x: List[float]) -> float:
    return 1.0 - (x[0]**2 + x[1]**2) / 225.0

def c2(x: List[float]) -> float:
    return (3.0 * x[1] - x[0]) / 10.0 - 1.0

# Create the problem with method chaining
problem = (OnTheFlyFloatProblem()
          .set_name("Srinivas")
          .add_variable(-20.0, 20.0)  # x1 in [-20, 20]
          .add_variable(-20.0, 20.0)  # x2 in [-20, 20]
          .add_function(f1)            # First objective
          .add_function(f2)            # Second objective
          .add_constraint(c1)          # First constraint (g1(x) <= 0)
          .add_constraint(c2))         # Second constraint (g2(x) <= 0)
Source code in src/jmetal/core/problem.py
def __init__(self):
    super().__init__()
    self.functions = []  #: List of objective functions to be minimized.
    self.constraints = []  #: List of constraint functions (<= 0).
    self.problem_name = None  #: Optional name for the problem.