diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 3c7fedaa..55ffd4e1 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -35,7 +35,7 @@ jobs: coverage xml genbadge coverage -i coverage.xml -o coverage.svg - name: Verify Changed files - uses: tj-actions/verify-changed-files@v16 + uses: tj-actions/verify-changed-files@v19 id: verify-changed-files with: files: coverage.svg diff --git a/README.md b/README.md index 3f0675ff..5a9136fe 100644 --- a/README.md +++ b/README.md @@ -41,16 +41,20 @@ next generation using the fitness values of all candidates it evaluated and rece It was already successfully applied in several accepted scientific publications. Applications include grid load forecasting, remote sensing, and structural molecular biology: -> D. Coquelin, R. Sedona, M. Riedel, and M. Götz. **Evolutionary Optimization of Neural Architectures in Remote Sensing -> Classification Problems**. IEEE International Geoscience and Remote Sensing Symposium IGARSS, Brussels, Belgium, -> pp. 1587-1590 (2021). https://doi.org/10.1109/IGARSS47720.2021.9554309 - > O. Taubert, F. von der Lehr, A. Bazarova, et al. **RNA contact prediction by data efficient deep learning**. Commun > Biol 6, 913 (2023). https://doi.org/10.1038/s42003-023-05244-9 > D. Coquelin, K. Flügel, M. Weiel, et al. **Harnessing Orthogonality to Train Low-Rank Neural Networks**. arXiv > preprint (2023). https://doi.org/10.48550/arXiv.2401.08505 +> Y. Funk, M. Götz, & H. Anzt. **Prediction of optimal solvers for sparse linear systems using deep learning**. +> Proceedings of the 2022 SIAM Conference on Parallel Processing for Scientific Computing (pp. 14-24). Society for +> Industrial and Applied Mathematics (2022). https://doi.org/10.1137/1.9781611977141.2 + +> D. Coquelin, R. Sedona, M. Riedel, and M. Götz. **Evolutionary Optimization of Neural Architectures in Remote Sensing +> Classification Problems**. IEEE International Geoscience and Remote Sensing Symposium IGARSS, Brussels, Belgium, +> pp. 1587-1590 (2021). https://doi.org/10.1109/IGARSS47720.2021.9554309 + ## In more technical terms ``Propulate`` is a massively parallel evolutionary hyperparameter optimizer based on the island model with asynchronous diff --git a/coverage.svg b/coverage.svg index bb1e512b..8ef24c92 100644 --- a/coverage.svg +++ b/coverage.svg @@ -1 +1 @@ -coverage: 79.89%coverage79.89% \ No newline at end of file +coverage: 86.84%coverage86.84% \ No newline at end of file diff --git a/propulate/__init__.py b/propulate/__init__.py index 172eef6e..28bfa465 100644 --- a/propulate/__init__.py +++ b/propulate/__init__.py @@ -1,14 +1,12 @@ -# -*- coding: utf-8 -*- -from pkg_resources import DistributionNotFound, get_distribution +from importlib.metadata import PackageNotFoundError, version try: # Change here if project is renamed and does not equal the package name - dist_name = __name__ - __version__ = get_distribution(dist_name).version -except DistributionNotFound: + __version__ = version(__name__) +except PackageNotFoundError: __version__ = "unknown" finally: - del get_distribution, DistributionNotFound + del version, PackageNotFoundError from . import propagators from .islands import Islands diff --git a/tests/test_cmaes.py b/tests/test_cmaes.py index d43d29b4..76c6a87b 100644 --- a/tests/test_cmaes.py +++ b/tests/test_cmaes.py @@ -1,56 +1,36 @@ import pathlib import random -from typing import Tuple import pytest from propulate import Propulator -from propulate.propagators import BasicCMA, CMAPropagator +from propulate.propagators import ActiveCMA, BasicCMA, CMAPropagator from propulate.utils.benchmark_functions import get_function_search_space -@pytest.fixture( - params=[ - ("rosenbrock", 0.0), - ("step", -25.0), - ("quartic", 0.0), - ("rastrigin", 0.0), - ("griewank", 0.0), - ("schwefel", 0.0), - ("bisphere", 0.0), - ("birastrigin", 0.0), - ("bukin", 0.0), - ("eggcrate", -1.0), - ("himmelblau", 0.0), - ("keane", 0.6736675), - ("leon", 0.0), - ("sphere", 0.0), # (fname, expected) - ] -) -def function_parameters(request): - """Define benchmark function parameter sets as used in tests.""" +@pytest.fixture(params=[BasicCMA(), ActiveCMA()]) +def cma_adapter(request): + """Iterate over CMA adapters (basic and active).""" return request.param -def test_cmaes( - function_parameters: Tuple[str, float], mpi_tmp_path: pathlib.Path -) -> None: +def test_cmaes_basic(cma_adapter, mpi_tmp_path: pathlib.Path) -> None: """ - Test Propulator to optimize a benchmark function using a CMA-ES propagator. + Test Propulator to optimize a benchmark function using CMA-ES propagators. This test is run both sequentially and in parallel. Parameters ---------- - function_parameters : Tuple[str, float] - The tuple containing each function name along with its global minimum. + cma_adapter : CMAAdapter + The CMA adapter used, either basic or active. mpi_tmp_path : pathlib.Path The temporary checkpoint directory. """ rng = random.Random(42) # Separate random number generator for optimization. - function, limits = get_function_search_space(function_parameters[0]) + function, limits = get_function_search_space("sphere") # Set up evolutionary operator. - adapter = BasicCMA() + adapter = cma_adapter propagator = CMAPropagator(adapter, limits, rng=rng) # Set up Propulator performing actual optimization. diff --git a/tests/test_island.py b/tests/test_island.py index a89fab65..368f9907 100644 --- a/tests/test_island.py +++ b/tests/test_island.py @@ -1,7 +1,7 @@ import copy import pathlib import random -from typing import Tuple +from typing import Callable, Dict, Tuple import deepdiff import numpy as np @@ -9,60 +9,62 @@ from mpi4py import MPI from propulate import Islands +from propulate.propagators import Propagator from propulate.utils import get_default_propagator, set_logger_config from propulate.utils.benchmark_functions import get_function_search_space +@pytest.fixture(scope="module") +def global_variables(): + """Get global variables used by most of the tests in this module.""" + rng = random.Random( + 42 + MPI.COMM_WORLD.rank + ) # Set up separate random number generator for optimization. + function, limits = get_function_search_space( + "sphere" + ) # Get function and search space to optimize. + propagator = get_default_propagator( + pop_size=4, + limits=limits, + rng=rng, + ) # Set up evolutionary operator. + yield rng, function, limits, propagator + + @pytest.fixture( params=[ - ("rosenbrock", 0.0), - ("step", -25.0), - ("quartic", 0.0), - ("rastrigin", 0.0), - ("griewank", 0.0), - ("schwefel", 0.0), - ("bisphere", 0.0), - ("birastrigin", 0.0), - ("bukin", 0.0), - ("eggcrate", -1.0), - ("himmelblau", 0.0), - ("keane", 0.6736675), - ("leon", 0.0), - ("sphere", 0.0), # (fname, expected) + True, + False, ] ) -def function_parameters(request): - """Define benchmark function parameter sets as used in tests.""" +def pollination(request): + """Iterate through pollination parameter.""" return request.param @pytest.mark.mpi(min_size=4) def test_islands( - function_parameters: Tuple[str, float], mpi_tmp_path: pathlib.Path + global_variables: Tuple[ + random.Random, Callable, Dict[str, Tuple[float, float]], Propagator + ], + pollination: bool, + mpi_tmp_path: pathlib.Path, ) -> None: """ Test basic island functionality (only run in parallel with at least four processes). Parameters ---------- - function_parameters : Tuple[str, float] - The tuple containing each function name along with its global minimum. + global_variables : Tuple[random.Random, Callable, Dict[str, Tuple[float, float]], propulate.Propagator] + Global variables used by most of the tests in this module. + pollination : bool + Whether pollination or real migration should be used. mpi_tmp_path : pathlib.Path The temporary checkpoint directory. """ - rng = random.Random( - 42 + MPI.COMM_WORLD.rank - ) # Separate random number generator for optimization - function, limits = get_function_search_space(function_parameters[0]) + rng, function, limits, propagator = global_variables set_logger_config(log_file=mpi_tmp_path / "log.log") - # Set up evolutionary operator. - propagator = get_default_propagator( - pop_size=4, - limits=limits, - rng=rng, - ) - # Set up island model. islands = Islands( loss_fn=function, @@ -71,45 +73,36 @@ def test_islands( generations=100, num_islands=2, migration_probability=0.9, - pollination=False, + pollination=pollination, checkpoint_path=mpi_tmp_path, ) # Run actual optimization. islands.evolve( - top_n=1, - logging_interval=10, debug=2, ) @pytest.mark.mpi(min_size=4) def test_checkpointing_isolated( - function_parameters: Tuple[str, float], mpi_tmp_path: pathlib.Path + global_variables: Tuple[ + random.Random, Callable, Dict[str, Tuple[float, float]], Propagator + ], + mpi_tmp_path: pathlib.Path, ) -> None: """ Test isolated island checkpointing without migration (only run in parallel with at least four processes). Parameters ---------- - function_parameters : Tuple[str, float] - The tuple containing each function name along with its global minimum. + global_variables : Tuple[random.Random, Callable, Dict[str, Tuple[float, float]], propulate.Propagator] + Global variables used by most of the tests in this module. mpi_tmp_path : pathlib.Path The temporary checkpoint directory. """ - rng = random.Random( - 42 + MPI.COMM_WORLD.rank - ) # Separate random number generator for optimization - function, limits = get_function_search_space(function_parameters[0]) + rng, function, limits, propagator = global_variables set_logger_config(log_file=mpi_tmp_path / "log.log") - # Set up evolutionary operator. - propagator = get_default_propagator( - pop_size=4, - limits=limits, - rng=rng, - ) - # Set up island model. islands = Islands( loss_fn=function, @@ -122,11 +115,7 @@ def test_checkpointing_isolated( ) # Run actual optimization. - islands.evolve( - top_n=1, - logging_interval=10, - debug=2, - ) + islands.evolve(top_n=1, debug=2) old_population = copy.deepcopy(islands.propulator.population) del islands @@ -152,102 +141,28 @@ def test_checkpointing_isolated( @pytest.mark.mpi(min_size=4) -def test_checkpointing_migration( - function_parameters: Tuple[str, float], mpi_tmp_path: pathlib.Path -) -> None: - """ - Test island checkpointing with migration (only run in parallel with at least four processes). - - Parameters - ---------- - function_parameters : Tuple[str, float] - The tuple containing each function name along with its global minimum. - mpi_tmp_path : pathlib.Path - The temporary checkpoint directory. - """ - rng = random.Random( - 42 + MPI.COMM_WORLD.rank - ) # Separate random number generator for optimization - function, limits = get_function_search_space(function_parameters[0]) - set_logger_config(log_file=mpi_tmp_path / "log.log") - - # Set up evolutionary operator. - propagator = get_default_propagator( - pop_size=4, - limits=limits, - rng=rng, - ) - - # Set up island model. - islands = Islands( - loss_fn=function, - propagator=propagator, - rng=rng, - generations=100, - num_islands=2, - migration_probability=0.9, - pollination=False, # TODO fixtureize - checkpoint_path=mpi_tmp_path, - ) - - # Run actual optimization. - islands.evolve( - top_n=1, - logging_interval=10, - debug=2, - ) - - old_population = copy.deepcopy(islands.propulator.population) - del islands - - islands = Islands( - loss_fn=function, - propagator=propagator, - rng=rng, - generations=100, - num_islands=2, - migration_probability=0.9, - pollination=False, # TODO fixtureize - checkpoint_path=mpi_tmp_path, - ) - - assert ( - len( - deepdiff.DeepDiff( - old_population, islands.propulator.population, ignore_order=True - ) - ) - == 0 - ) - - -@pytest.mark.mpi(min_size=4) -def test_checkpointing_pollination( - function_parameters: Tuple[str, float], mpi_tmp_path: pathlib.Path +def test_checkpointing( + global_variables: Tuple[ + random.Random, Callable, Dict[str, Tuple[float, float]], Propagator + ], + pollination: bool, + mpi_tmp_path: pathlib.Path, ) -> None: """ - Test island checkpointing with pollination (only run in parallel with at least four processes). + Test island checkpointing with migration and pollination (only run in parallel with at least four processes). Parameters ---------- - function_parameters : Tuple[str, float] - The tuple containing each function name along with its global minimum. + global_variables : Tuple[random.Random, Callable, Dict[str, Tuple[float, float]], propulate.Propagator] + Global variables used by most of the tests in this module. + pollination : bool + Whether pollination or real migration should be used. mpi_tmp_path : pathlib.Path The temporary checkpoint directory. """ - rng = random.Random( - 42 + MPI.COMM_WORLD.rank - ) # Separate random number generator for optimization - function, limits = get_function_search_space(function_parameters[0]) + rng, function, limits, propagator = global_variables set_logger_config(log_file=mpi_tmp_path / "log.log") - # Set up evolutionary operator. - propagator = get_default_propagator( - pop_size=4, - limits=limits, - rng=rng, - ) - # Set up island model. islands = Islands( loss_fn=function, @@ -256,14 +171,13 @@ def test_checkpointing_pollination( generations=100, num_islands=2, migration_probability=0.9, - pollination=False, # TODO fixtureize + pollination=pollination, checkpoint_path=mpi_tmp_path, ) # Run actual optimization. islands.evolve( top_n=1, - logging_interval=10, debug=2, ) @@ -277,7 +191,7 @@ def test_checkpointing_pollination( generations=100, num_islands=2, migration_probability=0.9, - pollination=True, # TODO fixtureize + pollination=pollination, checkpoint_path=mpi_tmp_path, ) @@ -293,31 +207,27 @@ def test_checkpointing_pollination( @pytest.mark.mpi(min_size=8) def test_checkpointing_unequal_populations( - function_parameters: Tuple[str, float], mpi_tmp_path: pathlib.Path + global_variables: Tuple[ + random.Random, Callable, Dict[str, Tuple[float, float]], Propagator + ], + pollination: bool, + mpi_tmp_path: pathlib.Path, ) -> None: """ Test island checkpointing for inhomogeneous island sizes (only run in parallel with at least eight processes). Parameters ---------- - function_parameters : Tuple[str, float] - The tuple containing each function name along with its global minimum. + global_variables : Tuple[random.Random, Callable, Dict[str, Tuple[float, float]], propulate.Propagator] + Global variables used by most of the tests in this module. + pollination : bool + Whether pollination or real migration should be used. mpi_tmp_path : pathlib.Path The temporary checkpoint directory. """ - rng = random.Random( - 42 + MPI.COMM_WORLD.rank - ) # Separate random number generator for optimization - function, limits = get_function_search_space(function_parameters[0]) + rng, function, limits, propagator = global_variables set_logger_config(log_file=mpi_tmp_path / "log.log") - # Set up evolutionary operator. - propagator = get_default_propagator( - pop_size=4, - limits=limits, - rng=rng, - ) - # Set up island model. islands = Islands( loss_fn=function, @@ -327,14 +237,13 @@ def test_checkpointing_unequal_populations( num_islands=2, island_sizes=np.array([3, 5]), migration_probability=0.9, - pollination=False, # TODO fixtureize + pollination=pollination, checkpoint_path=mpi_tmp_path, ) # Run actual optimization. islands.evolve( top_n=1, - logging_interval=10, debug=2, ) @@ -349,7 +258,7 @@ def test_checkpointing_unequal_populations( num_islands=2, island_sizes=np.array([3, 5]), migration_probability=0.9, - pollination=True, # TODO fixtureize + pollination=pollination, checkpoint_path=mpi_tmp_path, ) diff --git a/tests/test_propulator.py b/tests/test_propulator.py index caff7e50..3b04db3e 100644 --- a/tests/test_propulator.py +++ b/tests/test_propulator.py @@ -1,7 +1,6 @@ import copy import pathlib import random -from typing import Tuple import deepdiff import pytest @@ -14,30 +13,28 @@ @pytest.fixture( params=[ - ("rosenbrock", 0.0), - ("step", -25.0), - ("quartic", 0.0), - ("rastrigin", 0.0), - ("griewank", 0.0), - ("schwefel", 0.0), - ("bisphere", 0.0), - ("birastrigin", 0.0), - ("bukin", 0.0), - ("eggcrate", -1.0), - ("himmelblau", 0.0), - ("keane", 0.6736675), - ("leon", 0.0), - ("sphere", 0.0), # (fname, expected) + "rosenbrock", + "step", + "quartic", + "rastrigin", + "griewank", + "schwefel", + "bisphere", + "birastrigin", + "bukin", + "eggcrate", + "himmelblau", + "keane", + "leon", + "sphere", ] ) -def function_parameters(request): +def function_name(request): """Define benchmark function parameter sets as used in tests.""" return request.param -def test_propulator( - function_parameters: Tuple[str, float], mpi_tmp_path: pathlib.Path -) -> None: +def test_propulator(function_name: str, mpi_tmp_path: pathlib.Path) -> None: """ Test standard Propulator to optimize the benchmark functions using the default genetic propagator. @@ -45,15 +42,15 @@ def test_propulator( Parameters ---------- - function_parameters : Tuple[str, float] - The tuple containing each function name along with its global minimum. + function_name : str + The function name. mpi_tmp_path : pathlib.Path The temporary checkpoint directory. """ rng = random.Random( 42 + MPI.COMM_WORLD.rank ) # Random number generator for optimization - function, limits = get_function_search_space(function_parameters[0]) + function, limits = get_function_search_space(function_name) set_logger_config(log_file=mpi_tmp_path / "log.log") propagator = get_default_propagator( pop_size=4, @@ -94,7 +91,7 @@ def test_propulator_checkpointing(mpi_tmp_path: pathlib.Path) -> None: propulator = Propulator( loss_fn=function, propagator=propagator, - generations=1000, + generations=100, checkpoint_path=mpi_tmp_path, rng=rng, ) # Set up propulator performing actual optimization. diff --git a/tests/test_pso.py b/tests/test_pso.py index e3049a76..24ca5690 100644 --- a/tests/test_pso.py +++ b/tests/test_pso.py @@ -1,68 +1,80 @@ import pathlib import random -from typing import Tuple import pytest from mpi4py import MPI from propulate import Propulator from propulate.propagators import Conditional -from propulate.propagators.pso import BasicPSO, InitUniformPSO -from propulate.utils.benchmark_functions import get_function_search_space +from propulate.propagators.pso import ( + BasicPSO, + CanonicalPSO, + ConstrictionPSO, + InitUniformPSO, + VelocityClampingPSO, +) +from propulate.utils.benchmark_functions import get_function_search_space, sphere + +limits = get_function_search_space("sphere")[1] +rank = MPI.COMM_WORLD.rank +rng = random.Random(42 + rank) @pytest.fixture( params=[ - ("rosenbrock", 0.0), - ("step", -25.0), - ("quartic", 0.0), - ("rastrigin", 0.0), - ("griewank", 0.0), - ("schwefel", 0.0), - ("bisphere", 0.0), - ("birastrigin", 0.0), - ("bukin", 0.0), - ("eggcrate", -1.0), - ("himmelblau", 0.0), - ("keane", 0.6736675), - ("leon", 0.0), - ("sphere", 0.0), # (fname, expected) + BasicPSO( + inertia=0.729, + c_cognitive=1.49445, + c_social=1.49445, + rank=rank, + limits=limits, + rng=rng, + ), + VelocityClampingPSO( + inertia=0.729, + c_cognitive=1.49445, + c_social=1.49445, + rank=rank, + limits=limits, + rng=rng, + v_limits=0.6, + ), + ConstrictionPSO( + c_cognitive=2.05, + c_social=2.05, + rank=rank, + limits=limits, + rng=rng, + ), + CanonicalPSO( + c_cognitive=2.05, c_social=2.05, rank=rank, limits=limits, rng=rng + ), ] ) -def function_parameters(request): - """Define benchmark function parameter sets as used in tests.""" +def pso_propagator(request): + """Iterate over PSO propagator variants.""" return request.param @pytest.mark.mpi -def test_pso(function_parameters: Tuple[str, float, float], mpi_tmp_path: pathlib.Path): +def test_pso(pso_propagator, mpi_tmp_path: pathlib.Path): """ Test single worker using Propulator to optimize a benchmark function using the default genetic propagator. Parameters ---------- - function_parameters : Tuple - The tuple containing each function name along with its global minimum. + pso_propagator : BasicPSO + The PSO propagator variant to test. mpi_tmp_path : pathlib.Path The temporary checkpoint directory. """ - rng = random.Random(42) # Separate random number generator for optimization. - function, limits = get_function_search_space(function_parameters[0]) # Set up evolutionary operator. - pso_propagator = BasicPSO( - inertia=0.729, - c_cognitive=1.49334, - c_social=1.49445, - rank=MPI.COMM_WORLD.rank, # MPI rank - limits=limits, - rng=rng, - ) - init = InitUniformPSO(limits, rng=rng, rank=MPI.COMM_WORLD.rank) + init = InitUniformPSO(limits, rng=rng, rank=rank) propagator = Conditional(1, pso_propagator, init) # Set up propulator performing actual optimization. propulator = Propulator( - loss_fn=function, + loss_fn=sphere, propagator=propagator, rng=rng, generations=100, diff --git a/tests/test_surrogate.py b/tests/test_surrogate.py index 8a08b6a9..59bec367 100644 --- a/tests/test_surrogate.py +++ b/tests/test_surrogate.py @@ -144,6 +144,8 @@ def get_data_loaders(batch_size: int, root=Path) -> Tuple[DataLoader, DataLoader ---------- batch_size : int The batch size. + root : Path + The root path. Returns ------- @@ -161,7 +163,7 @@ def get_data_loaders(batch_size: int, root=Path) -> Tuple[DataLoader, DataLoader shuffle=False, ) - if MPI.COMM_WORLD.Get_rank() == 0: # Only root downloads data. + if MPI.COMM_WORLD.rank == 0: # Only root downloads data. train_loader = DataLoader( dataset=MNIST( download=True, root=root, transform=data_transform, train=True @@ -175,7 +177,7 @@ def get_data_loaders(batch_size: int, root=Path) -> Tuple[DataLoader, DataLoader setattr(get_data_loaders, "barrier_called", True) - if MPI.COMM_WORLD.Get_rank() != 0: + if MPI.COMM_WORLD.rank != 0: train_loader = DataLoader( dataset=MNIST( download=False, root=root, transform=data_transform, train=True @@ -203,6 +205,8 @@ def ind_loss( ---------- params : Dict[str, int | float | str] The parameters to be optimized. + root : Path + The root path. Returns ------- @@ -346,6 +350,10 @@ def test_mnist_static(mpi_tmp_path): delattr(get_data_loaders, "barrier_called") +@pytest.mark.filterwarnings( + "ignore::DeprecationWarning", + match="Assigning the 'data' attribute is an inherently unsafe operation and will be removed in the future.", +) @pytest.mark.mpi(min_size=4) def test_mnist_dynamic(mpi_tmp_path): """Test static surrogate using a torch convolutional network on the MNIST dataset."""