jMetalPy

repository·main·Indexed 20 days ago

https://github.com/jmetal/jmetalpy

A Python framework for multi-objective optimization (v1.9.0) providing evolutionary algorithms, benchmark problems, and statistical analysis tools. It includes support for algorithms like NSGA-II, NSGA-III, and GDE3, as well as parallel computing via Apache Spark and Dask. The library features various encodings, quality indicators, and visualization tools for Pareto fronts.

Tokens
32.4K
Snippets
73
Records
93
Agent score
70%

What's inside jmetalpy

  1. Overview of jMetalPy features

    main

    jMetalPy (v1.9.0) is a comprehensive framework for multi-objective optimization. Key features include:

    • Algorithms: Local search, genetic algorithms, evolution strategies, simulated annealing, NSGA-II, NSGA-III, SMPSO, OMOPSO, MOEA/D, SMS-EMOA, SPEA2, and more.
    • Parallel Computing: Support for Apache Spark and Dask.
    • Benchmark Problems: ZDT1-6, DTLZ1-2, FDA, LZ09, LIR-CMOP, RWA, RE, and various unconstrained/constrained problems.
    • Encodings: Real, integer, binary, and permutations.
    • Operators: Selection (tournament, ranking, etc.), crossover (single-point, SBX), and mutation (bit-blip, polynomial, etc.).
    • Quality Indicators: Hypervolume, additive epsilon, GD, IGD, and IGD+.
    • Visualization: Real-time, static, or interactive Pareto front plotting.
    • Statistical Analysis: Experiment class for studies and hypothesis testing (frequentist and Bayesian).
  2. Use Object-Oriented Programming and the @property decorator

    main

    jMetalPy strictly follows the Object-oriented programming paradigm. Avoid imperative or functional programming styles.

    Attribute Access

    • Attributes should generally be public and accessed directly.
    • Do not use standard getter/setter methods (e.g., get_value()/set_value()) as they are not idiomatic in Python.
    • If you need to implement additional logic during attribute access, use the @property decorator or the property() function. This allows users to continue accessing the attribute directly while you maintain control over the logic.
    class MyClass:
        def __init__(self):
            self._value = 0
    
        @property
        def value(self) -> int:
            # Additional logic here
            return self._value
    
        @value.setter
        def value(self, new_val: int):
            # Additional logic here
            self._value = new_val
    
    # Usage remains direct:
    obj = MyClass()
    obj.value = 10  # Uses setter
    print(obj.value) # Uses getter
  3. Understand Archive types in jMetalPy

    main

    Archives are data structures used to store and manage collections of solutions during the optimization process. jMetalPy provides several implementations depending on your requirements:

    • Archive: The base abstract class for all archive implementations.
    • BoundedArchive: An archive that enforces a maximum size limit.
    • NonDominatedSolutionsArchive: An archive that maintains only the non-dominated solutions.
    • CrowdingDistanceArchive: An archive that uses crowding distance to maintain diversity.
    • DistanceBasedArchive: An adaptive archive that changes its selection strategy based on the number of objectives.
    • ArchiveWithReferencePoint: An archive implementation that utilizes a reference point.
  4. How observers work to extend algorithms

    main

    jMetalPy uses the observer pattern to allow users to extend algorithm functionality without modifying the algorithm's core code. Algorithms maintain a list of observers (dependents) that are automatically notified after each iteration, similar to event listeners.

    You can register an observer by accessing the algorithm's observable attribute and calling the register method.

    Common use cases include logging metrics (evaluations, objectives, computing time) or displaying real-time progress bars.

    # Example of registering a basic observer
    basic = BasicAlgorithmObserver(frequency=1.0)
    algorithm.observable.register(observer=basic)
  5. Use appropriate data structures and typing

    main

    The project uses modern Python typing and data structures. Follow these patterns:

    • Immutable Data: Use @dataclass(slots=True, frozen=True) for stateless, immutable data like configuration objects, DTOs, or parameter bundles.
      • CRITICAL: Do not use frozen=True for Solution or other mutable containers that algorithms modify in place (e.g., during crossover, mutation, or repair), as this forces expensive copy-on-write operations in evolutionary loops.
    • Discrete Choices: Prefer Enum for sets of discrete choices.
    • Typing Syntax: Use modern syntax such as | for unions, TypeAlias, Literal, Final, TypedDict, and Self.
    • Annotations: Annotate parameters, return types, and key variables in all new or modified code.
    • Note on Static Analysis: While the project encourages typing, mypy is not enforced in CI. However, contributors are encouraged to use it locally.
  6. Handle errors and resources correctly

    main

    The project follows specific patterns for error handling depending on the layer of the code:

    • Core Logic (Algorithms/Operators/Problems): Use standard exceptions. Raise specific exceptions like ValueError or TypeError for invalid inputs or unrecoverable errors.
    • I/O Boundaries (File parsing, CLI handling): Use the Ok[T] | Err result pattern. This is intended for callers who need to branch on failure without using exceptions.
    • Resource Management: Always use context managers (with statements) to manage resources.
    • Exception Scope: Avoid global exception handling; keep it limited to the entry point of the application.
  7. How DistanceBasedArchive works

    main

    DistanceBasedArchive is an adaptive archive designed to maintain diverse solution sets in multi-objective optimization. It automatically switches its selection strategy based on the number of objectives in the solutions it receives:

    • 2 objectives: Uses crowding distance selection to maximize diversity.
    • >2 objectives: Uses distance-based subset selection with normalization.

    This makes it a versatile tool for both bi-objective and many-objective optimization problems without requiring manual configuration changes when the objective count changes.

    from jmetal.util.archive import DistanceBasedArchive
    from jmetal.core.solution import FloatSolution
    
    # Automatically handles 2 objectives via crowding distance
    archive = DistanceBasedArchive(maximum_size=10)
    
    # Automatically handles >2 objectives via distance-based selection
    archive_many = DistanceBasedArchive(maximum_size=10)
  8. Core concepts in jMetalPy

    main

    To use jMetalPy effectively, understand these five core abstractions:

    • Problems: The mathematical definitions of what you want to optimize.
    • Algorithms: The specific methods or heuristics used to search for optimal solutions.
    • Operators: The fundamental building blocks used by algorithms, such as crossover, mutation, and selection.
    • Quality Indicators: Metrics used to evaluate the quality of the solutions found.
    • Experiments: The framework used for performing systematic comparisons between different algorithms.
  9. Implement Type Hinting and Generics (Python 3.6+)

    main

    The project requires explicit type definitions for all function arguments and return values.

    Type Hinting

    Always define types in parameters and return values:

    def my_method(self, param: str) -> int:
        return 0

    Abstract Classes and Interfaces

    • Use abc.ABCMeta to define abstract classes.
    • To define an interface, define a class where all methods are marked as abstract.

    Generics

    • Use Generic[...] to define generic classes.
    • Generic classes inherit from abc.ABCMeta, meaning they are also abstract and can contain abstract methods.
    • You can fix type variables when inheriting from a generic class.
    from typing import TypeVar, Generic
    from abc import ABC
    
    S = TypeVar('S')
    
    class MyGenericClass(Generic[S]):
        def process(self, item: S) -> None:
            pass
  10. Understand Hypervolume (HV) and Normalized Hypervolume (NHV) reference points

    main

    The hv and nhv indicators require a reference point that is dominated by all solutions in the front.

    • Automatic Generation: If no --ref-point is provided, the CLI automatically generates one using the maximum values of the reference front plus the specified --margin (default 0.1).
    • Normalized Data: For normalized data, the default reference point is [1.1, 1.1, ...].
    • NHV Calculation: Normalized Hypervolume is calculated as NHV = 1 - HV(front) / HV(reference). Note that NHV can be negative if the solution front dominates the reference front; values closer to 0 indicate better performance.
  11. Follow the Git Workflow for contributions

    main

    jMetalPy uses a specific branching model to manage development.

    Constant Branches

    • master: Contains production-ready code. Merges from develop or hotfix branches must include a TAG.
    • develop: The integration branch for the next release. Receives merges from feature/ and fix/ branches.

    Temporary Branches

    • feature/<task-id>-<description>: Used for new functionality. Created from develop and merged back into develop.
    • fix/<task-id>-<description>: Used for corrections. Created from develop and merged back into develop.
    • hotfix/<task-id>-<description>: Used for production emergencies. Created from master and merged into both master and develop.

    Contribution Steps

    1. Create a local branch and upload it to the remote server immediately to enable automated verification.
    2. Push frequently to trigger automated tests.
    3. Once development is complete and tests pass, create a Pull Request.
    4. Important: Remove your local and remote branch once the merge is finalized.

    Useful Commands

    git fetch --prune
    git fetch --prune
  12. Use DistanceBasedArchive to maintain diverse solutions

    main

    To use the DistanceBasedArchive, instantiate it with a maximum_size. You can then add solutions using the .add(solution) method, which returns a boolean indicating if the solution was added. Use .size() to get the current count and .get(index) to retrieve solutions.

    from jmetal.util.archive import DistanceBasedArchive
    from jmetal.core.solution import FloatSolution
    
    # Create archive with maximum size of 10
    archive = DistanceBasedArchive(maximum_size=10)
    
    # Create some example solutions
    solutions = []
    for i in range(20):
        solution = FloatSolution([], [], 2)  # 2 objectives
        solution.objectives = [i/20.0, 1.0 - i/20.0]  # Trade-off front
        solutions.append(solution)
    
    # Add solutions to archive
    for solution in solutions:
        was_added = archive.add(solution)
        print(f"Solution {solution.objectives} added: {was_added}")
    
    print(f"Final archive size: {archive.size()}")
    print("Selected solutions:")
    for i in range(archive.size()):
        sol = archive.get(i)
        print(f"  {sol.objectives}")
    from jmetal.util.archive import DistanceBasedArchive
    from jmetal.core.solution import FloatSolution
    
    # Create archive with maximum size of 10
    archive = DistanceBasedArchive(maximum_size=10)
    
    # Create some example solutions
    solutions = []
    for i in range(20):
        solution = FloatSolution([], [], 2)  # 2 objectives
        solution.objectives = [i/20.0, 1.0 - i/20.0]  # Trade-off front
        solutions.append(solution)
    
    # Add solutions to archive
    for solution in solutions:
        was_added = archive.add(solution)
        print(f"Solution {solution.objectives} added: {was_added}")
    
    print(f"Final archive size: {archive.size()}")
    print("Selected solutions:")
    for i in range(archive.size()):
        sol = archive.get(i)
        print(f"  {sol.objectives}")