diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml new file mode 100644 index 00000000..2ff8b7de --- /dev/null +++ b/.github/workflows/python-test.yml @@ -0,0 +1,55 @@ +name: Python test + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up MPI + uses: mpi4py/setup-mpi@v1 + with: + mpi: 'openmpi' + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff pytest pytest-cov + pip install -r requirements.txt + pip install . + - name: Lint with ruff + run: | + # stop the build if there are Python syntax errors or undefined names + ruff --output-format=github --select=E9,F63,F7,F82 --target-version=py39 . + # default set of ruff rules with GitHub Annotations + ruff --output-format=github --target-version=py39 . + - name: Test with pytest + run: | + pytest --cov=propulate + - name: Coverage Badge + uses: tj-actions/coverage-badge-py@v2 + + - name: Verify Changed files + uses: tj-actions/verify-changed-files@v16 + id: verify-changed-files + with: + files: coverage.svg + + - name: Commit files + if: steps.verify-changed-files.outputs.files_changed == 'true' + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add coverage.svg + git commit -m "Updated coverage.svg" + + - name: Push changes + if: steps.verify-changed-files.outputs.files_changed == 'true' + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.github_token }} + branch: ${{ github.ref }} diff --git a/coverage.svg b/coverage.svg new file mode 100644 index 00000000..15cf15b2 --- /dev/null +++ b/coverage.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + coverage + coverage + 0% + 0% + + diff --git a/propulate/propagators/cmaes.py b/propulate/propagators/cmaes.py index 079a9379..efdb4ed2 100644 --- a/propulate/propagators/cmaes.py +++ b/propulate/propagators/cmaes.py @@ -1,6 +1,5 @@ -import copy import random -from typing import List, Dict, Union, Tuple +from typing import List, Dict, Tuple import numpy as np diff --git a/propulate/propagators/pso.py b/propulate/propagators/pso.py index d4d9d670..7eff7721 100644 --- a/propulate/propagators/pso.py +++ b/propulate/propagators/pso.py @@ -127,8 +127,8 @@ def _prepare_data( else: particles.append(make_particle(individual)) logging.warning( - f"Got Individual instead of Particle. If this is on purpose, you can ignore this warning. " - f"Converted the Individual to Particle. Continuing." + "Got Individual instead of Particle. If this is on purpose, you can ignore this warning. " + "Converted the Individual to Particle. Continuing." ) own_p = [ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..c8f9c4c0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[tool.ruff] +select = ["E", "F"] +ignore = ["E501"] + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +line-length = 88 diff --git a/requirements.txt b/requirements.txt index cc454539..a8465062 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ cycler==0.11.0 deepdiff>=5.8.0 kiwisolver==1.4.2 -matplotlib==3.2.1 mpi4py==3.0.3 numpy ordered-set==4.1.0 diff --git a/setup.cfg b/setup.cfg index d91f9108..46b5ec4f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,6 @@ install_requires = cycler #==0.11.0 deepdiff #==5.8.0 kiwisolver #==1.4.2 - matplotlib #==3.2.1 mpi4py #==3.0.3 numpy #==1.22.3 ordered-set #==4.1.0 @@ -103,7 +102,8 @@ exclude = dist .eggs docs/conf.py -max-line-length = 250 +max-line-length = 88 +extend-ignore = E203, E501 [semantic_release] branch = "release" diff --git a/tests/dummy_test.py b/tests/dummy_test.py deleted file mode 100644 index 1030a2e3..00000000 --- a/tests/dummy_test.py +++ /dev/null @@ -1,6 +0,0 @@ -def func(x): - return x + 1 - - -def test_answer(): - assert func(3) == 4 diff --git a/tests/test_cmaes.py b/tests/test_cmaes.py new file mode 100644 index 00000000..2285ef0d --- /dev/null +++ b/tests/test_cmaes.py @@ -0,0 +1,60 @@ +import random +import tempfile +from typing import Dict +from operator import attrgetter + +import numpy as np + +from propulate import Propulator +from propulate.propagators import CMAPropagator, BasicCMA + + +def sphere(params: Dict[str, float]) -> float: + """ + Sphere function: continuous, convex, separable, differentiable, unimodal + + Input domain: -5.12 <= x, y <= 5.12 + Global minimum 0 at (x, y) = (0, 0) + + Parameters + ---------- + params: dict[str, float] + function parameters + Returns + ------- + float + function value + """ + return np.sum(np.array(list(params.values())) ** 2) + + +def test_PSO(): + """ + Test single worker using Propulator to optimize sphere using a PSO propagator. + """ + rng = random.Random(42) # Separate random number generator for optimization. + limits = { + "a": (-5.12, 5.12), + "b": (-5.12, 5.12), + } + with tempfile.TemporaryDirectory() as checkpoint_path: + # Set up evolutionary operator. + + adapter = BasicCMA() + propagator = CMAPropagator(adapter, limits, rng=rng) + + # Set up propulator performing actual optimization. + propulator = Propulator( + loss_fn=sphere, + propagator=propagator, + generations=10, + checkpoint_path=checkpoint_path, + rng=rng, + ) + + # Run optimization and print summary of results. + propulator.propulate() + propulator.summarize() + best = min(propulator.population, key=attrgetter("loss")) + + assert best.loss < 10.0 diff --git a/tests/test_propulator_sphere.py b/tests/test_propulator_sphere.py new file mode 100644 index 00000000..d6fae54e --- /dev/null +++ b/tests/test_propulator_sphere.py @@ -0,0 +1,73 @@ +import random +import tempfile +from typing import Dict +from operator import attrgetter +import logging + +import numpy as np + +from propulate import Propulator +from propulate.utils import get_default_propagator, set_logger_config + + +def sphere(params: Dict[str, float]) -> float: + """ + Sphere function: continuous, convex, separable, differentiable, unimodal + + Input domain: -5.12 <= x, y <= 5.12 + Global minimum 0 at (x, y) = (0, 0) + + Parameters + ---------- + params: dict[str, float] + function parameters + Returns + ------- + float + function value + """ + return np.sum(np.array(list(params.values())) ** 2) + + +def test_Propulator(): + """ + Test single worker using Propulator to optimize sphere. + """ + rng = random.Random(42) # Separate random number generator for optimization. + limits = { + "a": (-5.12, 5.12), + "b": (-5.12, 5.12), + } + with tempfile.TemporaryDirectory() as checkpoint_path: + set_logger_config( + level=logging.INFO, + log_file=checkpoint_path + "/propulate.log", + log_to_stdout=True, + log_rank=False, + colors=True, + ) + # Set up evolutionary operator. + propagator = get_default_propagator( # Get default evolutionary operator. + pop_size=4, # Breeding pool size + limits=limits, # Search-space limits + mate_prob=0.7, # Crossover probability + mut_prob=9.0, # Mutation probability + random_prob=0.1, # Random-initialization probability + rng=rng, # Random number generator + ) + + # Set up propulator performing actual optimization. + propulator = Propulator( + loss_fn=sphere, + propagator=propagator, + generations=10, + checkpoint_path=checkpoint_path, + rng=rng, + ) + + # Run optimization and print summary of results. + propulator.propulate() + propulator.summarize() + best = min(propulator.population, key=attrgetter("loss")) + + assert best.loss < 0.8 diff --git a/tests/test_pso.py b/tests/test_pso.py new file mode 100644 index 00000000..437384c3 --- /dev/null +++ b/tests/test_pso.py @@ -0,0 +1,69 @@ +import random +import tempfile +from typing import Dict +from operator import attrgetter + +import numpy as np + +from propulate import Propulator +from propulate.propagators import Conditional +from propulate.propagators.pso import BasicPSO, InitUniformPSO + + +def sphere(params: Dict[str, float]) -> float: + """ + Sphere function: continuous, convex, separable, differentiable, unimodal + + Input domain: -5.12 <= x, y <= 5.12 + Global minimum 0 at (x, y) = (0, 0) + + Parameters + ---------- + params: dict[str, float] + function parameters + Returns + ------- + float + function value + """ + return np.sum(np.array(list(params.values())) ** 2) + + +def test_PSO(): + """ + Test single worker using Propulator to optimize sphere using a PSO propagator. + """ + rng = random.Random(42) # Separate random number generator for optimization. + limits = { + "a": (-5.12, 5.12), + "b": (-5.12, 5.12), + } + with tempfile.TemporaryDirectory() as checkpoint_path: + # Set up evolutionary operator. + + pso_propagator = BasicPSO( + 0.729, + 1.49334, + 1.49445, + 0, # MPI rank TODO fix when implemented proper MPI parallel tests + limits, + rng, + ) + init = InitUniformPSO(limits, rng=rng, rank=0) + propagator = Conditional(1, pso_propagator, init) # TODO MPIify + + # Set up propulator performing actual optimization. + propulator = Propulator( + loss_fn=sphere, + propagator=propagator, + generations=10, + checkpoint_path=checkpoint_path, + rng=rng, + ) + + # Run optimization and print summary of results. + propulator.propulate() + propulator.summarize() + best = min(propulator.population, key=attrgetter("loss")) + + assert best.loss < 30.0 diff --git a/tutorials/cmaes_example.py b/tutorials/cmaes_example.py index d67d93ca..64ec2d66 100644 --- a/tutorials/cmaes_example.py +++ b/tutorials/cmaes_example.py @@ -8,7 +8,7 @@ from propulate import Propulator from propulate.propagators import BasicCMA, ActiveCMA, CMAPropagator from propulate.utils import set_logger_config -from function_benchmark import * +from function_benchmark import get_function_search_space if __name__ == "__main__": diff --git a/tutorials/islands_example.py b/tutorials/islands_example.py index 2022ffe4..e6b595c7 100755 --- a/tutorials/islands_example.py +++ b/tutorials/islands_example.py @@ -2,13 +2,14 @@ import argparse import logging import random + +import numpy as np from mpi4py import MPI from propulate import Islands from propulate.propagators import SelectMin, SelectMax from propulate.utils import get_default_propagator, set_logger_config -from function_benchmark import * - +from function_benchmark import get_function_search_space if __name__ == "__main__": comm = MPI.COMM_WORLD diff --git a/tutorials/propulator_example.py b/tutorials/propulator_example.py index b4d3dd24..58b05453 100755 --- a/tutorials/propulator_example.py +++ b/tutorials/propulator_example.py @@ -7,7 +7,7 @@ from propulate import Propulator from propulate.utils import get_default_propagator, set_logger_config -from function_benchmark import * +from function_benchmark import get_function_search_space if __name__ == "__main__": comm = MPI.COMM_WORLD