diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index b0d1ce9..0000000 --- a/.editorconfig +++ /dev/null @@ -1,8 +0,0 @@ -root = true - -[*.py] -charset = utf-8 -indent_style = space -indent_size = 4 -insert_final_newline = true -end_of_line = lf diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml new file mode 100644 index 0000000..1ddf084 --- /dev/null +++ b/.github/workflows/publish_to_pypi.yml @@ -0,0 +1,30 @@ +name: Upload new PaDELPy version to PyPI + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 0000000..b2b19a8 --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,26 @@ +name: Run ECabc tests + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + pip install pytest pytest-md + - name: Install package + run: python -m pip install . + - name: Run tests + uses: pavelzw/pytest-action@v2 + with: + emoji: false + report-title: 'ECabc test report' \ No newline at end of file diff --git a/README.md b/README.md index 774853f..cd8447f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ [![PyPI version](https://badge.fury.io/py/ecabc.svg)](https://badge.fury.io/py/ecabc) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ECRL/ecabc/blob/master/LICENSE) [![DOI](http://joss.theoj.org/papers/10.21105/joss.01420/status.svg)](https://doi.org/10.21105/joss.01420) -[![Build Status](https://dev.azure.com/uml-ecrl/package-management/_apis/build/status/ECRL.ecabc?branchName=master)](https://dev.azure.com/uml-ecrl/package-management/_build/latest?definitionId=5&branchName=master) **ECabc** is an open source Python package used to tune parameters for user-supplied functions based on the [Artificial Bee Colony by D. Karaboğa](http://scholarpedia.org/article/Artificial_bee_colony_algorithm). ECabc optimizes user supplied functions, or **fitness function**s, using a set of variables that exist within a search space. The bee colony consists of three types of bees: employers, onlookers and scouts. An **employer bee** exploits a solution comprised of a permutation of the variables in the search space, and evaluates the viability of the solution. An **onlooker bee** chooses an employer bee with an optimal solution and searches for new solutions near them. The **scout bee**, a variant of the employer bee, will search for a new solution if it has stayed too long at its current solution. @@ -43,7 +42,7 @@ Note: if multiple Python releases are installed on your system (e.g. 2.7 and 3.7 ### Method 2: From source - Download the ECabc repository, navigate to the download location on the command line/terminal, and execute: ``` -python setup.py install +pip install . ``` There are currently no additional dependencies for ECabc. diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index da688ea..0000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,21 +0,0 @@ -trigger: -- master - -pool: - vmImage: 'ubuntu-latest' - -steps: -- task: UsePythonVersion@0 - inputs: - versionSpec: '3.7' - architecture: 'x64' - -- script: | - python -m pip install --upgrade pip setuptools wheel - python setup.py install - displayName: 'Install dependencies' - -- script: | - cd tests - python test_all.py - displayName: 'Unit Tests' diff --git a/images/state.png b/images/state.png deleted file mode 100644 index 7cb0690..0000000 Binary files a/images/state.png and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9ac716f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +exclude = ["paper*"] + +[project] +name = "ecabc" +version = "3.0.1" +authors = [ + { name="Sanskriti Sharma", email="Sanskriti_Sharma@student.uml.edu" }, + { name="Hernan Gelaf-Romer", email="Hernan_Gelafromer@student.uml.edu" }, + { name="Travis Kessler", email="travis.j.kessler@gmail.com" }, +] +description = "Artificial bee colony for function parameter optimization" +readme = "README.md" +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://github.com/ecrl/ecabc" +"Bug Tracker" = "https://github.com/ecrl/ecabc/issues" \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 480533d..0000000 --- a/setup.py +++ /dev/null @@ -1,16 +0,0 @@ -from setuptools import setup - -setup( - name='ecabc', - version='3.0.0', - description='Artificial bee colony for function parameter optimization', - url='https://github.com/ECRL/ecabc', - author='Sanskriti Sharma, Hernan Gelaf-Romer, Travis Kessler', - author_email='Sanskriti_Sharma@student.uml.edu, ' - 'Hernan_Gelafromer@student.uml.edu, ' - 'Travis_Kessler@student.uml.edu', - license='MIT', - packages=['ecabc'], - install_requires=[], - zip_safe=False -) diff --git a/tests/test_all.py b/tests/test_all.py index 0b89926..c9f2d61 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -1,11 +1,235 @@ -import unittest +import pytest -from unit_tests.bee import TestBee -from unit_tests.colony import TestColony -from unit_tests.parameter import TestParameter -from unit_tests.utils import TestUtils +import ecabc.utils as abc_utils +from ecabc import ABC, Bee, Parameter -if __name__ == '__main__': +def _objective_function(params): + return sum(params) - unittest.main() + +def _objective_function_kwargs(params, my_kwarg): + return sum(params) + my_kwarg + + +# utils.py + + +def test_apply_mutation(): + params = [Parameter(0, 10, True) for _ in range(3)] + curr_params = [5, 5, 5] + mut_params = abc_utils.apply_mutation(curr_params, params) + assert curr_params != mut_params + + +def test_call_obj_fn(): + param_vals = [2, 2, 2] + ret_params, result = abc_utils.call_obj_fn( + param_vals, _objective_function, {} + ) + assert result == 6 + assert ret_params == param_vals + fn_args = {'my_kwarg': 2} + ret_params, result = abc_utils.call_obj_fn( + param_vals, _objective_function_kwargs, fn_args + ) + assert result == 8 + assert param_vals == ret_params + + +def test_choose_bee(): + + bee_1 = Bee([0], 100000000000000000000, 0) + bee_2 = Bee([0], 0, 0) + chosen_bee = abc_utils.choose_bee([bee_1, bee_2]) + assert chosen_bee == bee_2 + + +def test_determine_best_bee(): + + bee_1 = Bee([10], 10, 0) + bee_2 = Bee([0], 0, 0) + bee_2_fitness = bee_2._fitness_score + bee_2_ret_val = bee_2._obj_fn_val + bee_2_params = bee_2._params + _fit, _ret, _param = abc_utils.determine_best_bee([bee_1, bee_2]) + assert _fit == bee_2_fitness + assert _ret == bee_2_ret_val + assert _param == bee_2_params + + +# parameter.py + + +def test_param_init(): + param = Parameter(0, 10, False) + assert param._restrict is False + param = Parameter(0, 10) + assert param._restrict is True + assert param._dtype == int + assert param._min_val == 0 + assert param._max_val == 10 + + +def test_rand_val(): + for _ in range(100): + param = Parameter(0, 10) + rand_val = param.rand_val + assert rand_val >= 0 + assert rand_val <= 10 + assert type(rand_val) == int + param = Parameter(0.0, 10.0) + rand_val = param.rand_val + assert type(rand_val) == float + + +def test_mutate(): + param = Parameter(0, 10) + rand_val = param.rand_val + for _ in range(1000): + mutation = param.mutate(rand_val) + if mutation < 0 or mutation > 10: + raise ValueError('Mutation outside min/max bounds') + param = Parameter(0, 3, False) + rand_val = param.rand_val + outside_bounds = False + mutation = param.mutate(rand_val) + while True: + if mutation < 0 or mutation > 3: + outside_bounds = True + break + mutation = param.mutate(mutation) + assert outside_bounds is True + + +# abc.py + + +def test_colony_init(): + c = ABC(10, _objective_function) + assert c._num_employers == 10 + assert c._obj_fn == _objective_function + kwargs = {'my_kwarg': 2} + c = ABC(10, _objective_function_kwargs, kwargs) + assert c._obj_fn_args == kwargs + c = ABC(10, _objective_function, num_processes=8) + assert c._num_processes == 8 + with pytest.raises(ReferenceError): + c = ABC(10, None) + + +def test_no_bees(): + c = ABC(10, _objective_function) + assert c.best_fitness == 0 + assert c.best_ret_val is None + assert c.best_params == {} + assert c.average_fitness is None + assert c.average_ret_val is None + with pytest.raises(RuntimeError): + c.search() + + +def test_add_parameter(): + c = ABC(10, _objective_function) + c.add_param(0, 1) + c.add_param(2, 3) + c.add_param(4, 5) + assert len(c._params) == 3 + assert c._params[0]._min_val == 0 + assert c._params[0]._max_val == 1 + assert c._params[1]._min_val == 2 + assert c._params[1]._max_val == 3 + assert c._params[2]._min_val == 4 + assert c._params[2]._max_val == 5 + assert c._params[0]._dtype == int + c.add_param(0.0, 1.0) + assert c._params[3]._dtype == float + assert c._params[0]._restrict is True + c.add_param(0, 1, False) + assert c._params[4]._restrict is False + + +def test_initialize(): + c = ABC(10, _objective_function) + c.add_param(0, 10) + c.add_param(0, 10) + c.initialize() + assert len(c._bees) == 20 + + +def test_search_and_stats(): + c = ABC(10, _objective_function) + c.add_param(0, 0) + c.add_param(0, 0) + c.initialize() + for _ in range(50): + c.search() + assert c.best_fitness == 1 + assert c.best_ret_val == 0 + assert c.best_params == {'P0': 0, 'P1': 0} + + +def test_kwargs(): + c = ABC(10, _objective_function_kwargs, {'my_kwarg': 2}) + c.add_param(0, 0) + c.add_param(0, 0) + c.initialize() + for _ in range(50): + c.search() + assert c.best_ret_val == 2 + + +def test_multiprocessing(): + c = ABC(20, _objective_function, num_processes=4) + assert c._num_processes == 4 + c.add_param(0, 10) + c.add_param(0, 10) + c.initialize() + c.search() + + +def test_custom_param_name(): + c = ABC(20, _objective_function) + c.add_param(0, 10, name='int1') + c.add_param(0, 10, name='int2') + c.initialize() + for _ in range(50): + c.search() + assert c.best_params == {'int1': 0, 'int2': 0} + + +# bee.py + + +def test_bee_init(): + bee = Bee([0, 0, 0], 0, 1, True) + assert bee._is_employer is True + bee = Bee([0, 0, 0], 0, 1) + assert bee._is_employer is False + assert bee._params == [0, 0, 0] + assert bee._obj_fn_val == 0 + assert bee._fitness_score == 1 + assert bee._stay_limit == 1 + + +def test_abandon(): + bee = Bee([0, 0, 0], 0, 2) + result = bee.abandon + assert result is False + result = bee.abandon + assert result is True + + +def test_calc_fitness(): + result = Bee.calc_fitness(0) + assert result == 1 + result = Bee.calc_fitness(-1) + assert result == 2 + + +def test_is_better_food(): + bee = Bee([0, 0, 1], 1, 1) + result = bee.is_better_food(0) + assert result is True + result = bee.is_better_food(2) + assert result is False diff --git a/tests/unit_tests/bee.py b/tests/unit_tests/bee.py deleted file mode 100644 index c1ef7cd..0000000 --- a/tests/unit_tests/bee.py +++ /dev/null @@ -1,48 +0,0 @@ -import unittest - -from ecabc import Bee - - -class TestBee(unittest.TestCase): - - def test_bee_init(self): - - bee = Bee([0, 0, 0], 0, 1, True) - self.assertTrue(bee._is_employer) - - bee = Bee([0, 0, 0], 0, 1) - self.assertFalse(bee._is_employer) - - bee = Bee([0, 0, 0], 0, 1) - self.assertEqual(bee._params, [0, 0, 0]) - self.assertEqual(bee._obj_fn_val, 0) - self.assertEqual(bee._fitness_score, 1) - self.assertEqual(bee._stay_limit, 1) - - def test_abandon(self): - - bee = Bee([0, 0, 0], 0, 2) - result = bee.abandon - self.assertFalse(result) - result = bee.abandon - self.assertTrue(result) - - def test_calc_fitness(self): - - result = Bee.calc_fitness(0) - self.assertEqual(result, 1) - result = Bee.calc_fitness(-1) - self.assertEqual(result, 2) - - def test_is_better_food(self): - - bee = Bee([0, 0, 1], 1, 1) - result = bee.is_better_food(0) - self.assertTrue(result) - result = bee.is_better_food(2) - self.assertFalse(result) - - -if __name__ == '__main__': - - unittest.main() diff --git a/tests/unit_tests/colony.py b/tests/unit_tests/colony.py deleted file mode 100644 index 3e83290..0000000 --- a/tests/unit_tests/colony.py +++ /dev/null @@ -1,130 +0,0 @@ -import unittest - -from ecabc import ABC - - -def objective_function(params): - return sum(params) - - -def objective_function_kwargs(params, my_kwarg): - return sum(params) + my_kwarg - - -class TestColony(unittest.TestCase): - - def test_colony_init(self): - - c = ABC(10, objective_function) - self.assertEqual(c._num_employers, 10) - self.assertEqual(c._obj_fn, objective_function) - - kwargs = {'my_kwarg', 2} - c = ABC(10, objective_function, kwargs) - self.assertEqual(c._obj_fn_args, kwargs) - - c = ABC(10, objective_function, num_processes=8) - self.assertEqual(c._num_processes, 8) - - try: - c = ABC(10, None) - except Exception as E: - self.assertEqual(type(E), ReferenceError) - - def test_no_bees(self): - - c = ABC(10, objective_function) - self.assertEqual(c.best_fitness, 0) - self.assertEqual(c.best_ret_val, None) - self.assertEqual(c.best_params, {}) - self.assertEqual(c.average_fitness, None) - self.assertEqual(c.average_ret_val, None) - - try: - c.search() - except Exception as E: - self.assertEqual(type(E), RuntimeError) - - def test_add_parameter(self): - - c = ABC(10, objective_function) - c.add_param(0, 1) - c.add_param(2, 3) - c.add_param(4, 5) - self.assertEqual(len(c._params), 3) - self.assertEqual(c._params[0]._min_val, 0) - self.assertEqual(c._params[0]._max_val, 1) - self.assertEqual(c._params[1]._min_val, 2) - self.assertEqual(c._params[1]._max_val, 3) - self.assertEqual(c._params[2]._min_val, 4) - self.assertEqual(c._params[2]._max_val, 5) - self.assertEqual(c._params[0]._dtype, int) - c.add_param(0.0, 1.0) - self.assertEqual(c._params[3]._dtype, float) - self.assertTrue(c._params[0]._restrict) - c.add_param(0, 1, False) - self.assertFalse(c._params[4]._restrict) - - def test_initialize(self): - - c = ABC(10, objective_function) - c.add_param(0, 10) - c.add_param(0, 10) - c.initialize() - self.assertEqual(len(c._bees), 20) - - def test_get_stats(self): - - c = ABC(10, objective_function) - c.add_param(0, 0) - c.add_param(0, 0) - c.initialize() - self.assertEqual(c.best_fitness, 1) - self.assertEqual(c.best_ret_val, 0) - self.assertEqual(c.best_params, {'P0': 0, 'P1': 0}) - self.assertEqual(c.average_fitness, 1) - self.assertEqual(c.average_ret_val, 0) - - def test_kwargs(self): - - c = ABC(10, objective_function_kwargs, {'my_kwarg': 2}) - c.add_param(0, 0) - c.add_param(0, 0) - c.initialize() - self.assertEqual(c.best_ret_val, 2) - - def test_search(self): - - c = ABC(20, objective_function) - c.add_param(0, 10) - c.add_param(0, 10) - c.initialize() - for _ in range(50): - c.search() - self.assertEqual(c.best_fitness, 1) - self.assertEqual(c.best_ret_val, 0) - self.assertEqual(c.best_params, {'P0': 0, 'P1': 0}) - - def test_multiprocessing(self): - - c = ABC(20, objective_function, num_processes=4) - self.assertEqual(c._num_processes, 4) - c.add_param(0, 10) - c.add_param(0, 10) - c.initialize() - c.search() - - def test_custom_param_name(self): - - c = ABC(20, objective_function) - c.add_param(0, 10, name='int1') - c.add_param(0, 10, name='int2') - c.initialize() - for _ in range(50): - c.search() - self.assertEqual(c.best_params, {'int1': 0, 'int2': 0}) - - -if __name__ == '__main__': - - unittest.main() diff --git a/tests/unit_tests/parameter.py b/tests/unit_tests/parameter.py deleted file mode 100644 index 437bfbb..0000000 --- a/tests/unit_tests/parameter.py +++ /dev/null @@ -1,63 +0,0 @@ -import unittest - -from ecabc import Parameter - - -class TestParameter(unittest.TestCase): - - def test_param_init(self): - - param = Parameter(0, 10, False) - self.assertFalse(param._restrict) - - param = Parameter(0, 10) - self.assertTrue(param._restrict) - - param = Parameter(0, 10) - self.assertEqual(param._dtype, int) - self.assertEqual(param._min_val, 0) - self.assertEqual(param._max_val, 10) - - param = Parameter(0.0, 10.0) - self.assertEqual(param._dtype, float) - self.assertEqual(param._min_val, 0.0) - self.assertEqual(param._max_val, 10.0) - - def test_rand_val(self): - - param = Parameter(0, 10) - rand_val = param.rand_val - self.assertGreaterEqual(rand_val, 0) - self.assertLessEqual(rand_val, 10) - self.assertEqual(type(rand_val), int) - - param = Parameter(0.0, 10.0) - rand_val = param.rand_val - self.assertEqual(type(rand_val), float) - - def test_mutate(self): - - param = Parameter(0, 10) - rand_val = param.rand_val - for _ in range(1000): - mutation = param.mutate(rand_val) - if mutation < 0 or mutation > 10: - raise ValueError('Mutation outside min/max bounds') - - param = Parameter(0, 3, False) - rand_val = param.rand_val - outside_bounds = False - mutation = param.mutate(rand_val) - while True: - if mutation < 0 or mutation > 3: - outside_bounds = True - break - mutation = param.mutate(mutation) - self.assertTrue( - outside_bounds, 'Mutation did not occur outside min/max bounds' - ) - - -if __name__ == '__main__': - - unittest.main() diff --git a/tests/unit_tests/utils.py b/tests/unit_tests/utils.py deleted file mode 100644 index afa04af..0000000 --- a/tests/unit_tests/utils.py +++ /dev/null @@ -1,59 +0,0 @@ -import unittest - -import ecabc.utils as abc_utils -from ecabc import Bee, Parameter - - -class TestUtils(unittest.TestCase): - - def test_apply_mutation(self): - - params = [Parameter(0, 10, True) for _ in range(3)] - curr_params = [5, 5, 5] - mut_params = abc_utils.apply_mutation(curr_params, params) - self.assertNotEqual(curr_params, mut_params) - - def test_call_obj_fn(self): - - def obj_fn(params): - return sum(params) - - def obj_fn_kw(params, my_kwarg): - return sum(params) + my_kwarg - - param_vals = [2, 2, 2] - - ret_params, result = abc_utils.call_obj_fn(param_vals, obj_fn, {}) - self.assertEqual(6, result) - self.assertEqual(param_vals, ret_params) - - fn_args = {'my_kwarg': 2} - ret_params, result = abc_utils.call_obj_fn( - param_vals, obj_fn_kw, fn_args - ) - self.assertEqual(8, result) - self.assertEqual(param_vals, ret_params) - - def test_choose_bee(self): - - bee_1 = Bee([0], 100000000000000000000, 0) - bee_2 = Bee([0], 0, 0) - chosen_bee = abc_utils.choose_bee([bee_1, bee_2]) - self.assertEqual(chosen_bee, bee_2) - - def test_determine_best_bee(self): - - bee_1 = Bee([10], 10, 0) - bee_2 = Bee([0], 0, 0) - bee_2_fitness = bee_2._fitness_score - bee_2_ret_val = bee_2._obj_fn_val - bee_2_params = bee_2._params - _fit, _ret, _param = abc_utils.determine_best_bee([bee_1, bee_2]) - self.assertEqual(_fit, bee_2_fitness) - self.assertEqual(_ret, bee_2_ret_val) - self.assertEqual(_param, bee_2_params) - - -if __name__ == '__main__': - - unittest.main()