From 32a0b99d01187eb1e09d93cb29654b4f20eccd24 Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Wed, 10 Mar 2021 21:46:58 -0500 Subject: [PATCH 01/13] Requirement of python>=2.6 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index f81bb5c..ef68272 100644 --- a/setup.py +++ b/setup.py @@ -33,5 +33,6 @@ 'numpy>=1.18.1', 'ortools>=7.8.7959' ], + python_requires=">=3.6", include_package_data=True, ) From 2c86fd30153497cbb45de16e7a98947ab04060ce Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Wed, 10 Mar 2021 21:47:39 -0500 Subject: [PATCH 02/13] travis-ci initial configuration --- .travis.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..223f927 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +language: python +python: + - 3.5 + - 3.6 + - 3.7 + - 3.8 + - 3.9 +before_install: + - python --version + - pip install -U pip +install: + - pip install dev-requirements.txt +script: + - pytest tests/ --cov-config=.coveragerc +after_success: + - codecov \ No newline at end of file From e42d25760a1e5c07b113f0d2991a6d6386c57af5 Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Wed, 10 Mar 2021 21:48:23 -0500 Subject: [PATCH 03/13] numpy and ortools requirements --- dev-requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 32cb212..90b9e89 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,6 @@ pytest==6.2.2 codecov==2.1.11 pytest-cov==2.11.1 -twine==2.4.7 \ No newline at end of file +twine==2.4.7 +numpy>=1.18.1, +ortools>=7.8.7959 From 7832688648f5b9174186cf79b37ca071e3179063 Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Wed, 10 Mar 2021 21:55:27 -0500 Subject: [PATCH 04/13] Fix pip install command --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 223f927..e152c37 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,8 @@ before_install: - python --version - pip install -U pip install: - - pip install dev-requirements.txt + - pip install -r dev-requirements.txt script: - pytest tests/ --cov-config=.coveragerc after_success: - - codecov \ No newline at end of file + - codecov From 9a545fad8f350b05794ab937225bd8c66d391877 Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Wed, 10 Mar 2021 21:56:18 -0500 Subject: [PATCH 05/13] travis-ci build status on main branch --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7c50ae1..4cca02e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://www.travis-ci.com/rodrigo-arenas/pyworkforce.svg?branch=main)](https://www.travis-ci.com/rodrigo-arenas/pyworkforce) + # pyworkforce Common tools for workforce management, schedule and optimization problems built in top of tools like google's ortools and custom modules From 461994a946b5521b42db2d5ae1dd9a1d1a9a4412 Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Wed, 10 Mar 2021 22:00:53 -0500 Subject: [PATCH 06/13] Fix twine required version --- dev-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 90b9e89..c548b95 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,6 @@ pytest==6.2.2 codecov==2.1.11 pytest-cov==2.11.1 -twine==2.4.7 -numpy>=1.18.1, +twine==3.3.0 +numpy>=1.18.1 ortools>=7.8.7959 From 5a481277c38f0a64c56c2b8ca66d5ad52a92a5ad Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Wed, 10 Mar 2021 22:04:07 -0500 Subject: [PATCH 07/13] Removed python 3.5 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e152c37..5d88afd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - 3.5 - 3.6 - 3.7 - 3.8 From 575e61a94d86999e5b8f10647d76afefc469faca Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Wed, 10 Mar 2021 22:20:43 -0500 Subject: [PATCH 08/13] Increased options in coverage --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5d88afd..9b8b139 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,6 @@ before_install: install: - pip install -r dev-requirements.txt script: - - pytest tests/ --cov-config=.coveragerc + - pytest tests/ --verbose --color=yes --assert=plain --cov-config=.coveragerc --cov=./ after_success: - codecov From 9a7a95217085bd76ef08aeb679caa9af1b3eaad9 Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Thu, 11 Mar 2021 11:43:36 -0500 Subject: [PATCH 09/13] Added pypi and python versions badges --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cca02e..3877093 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ + [![Build Status](https://www.travis-ci.com/rodrigo-arenas/pyworkforce.svg?branch=main)](https://www.travis-ci.com/rodrigo-arenas/pyworkforce) +[![PyPI Version](https://badge.fury.io/py/pyworkforce.svg)](https://badge.fury.io/py/pyworkforce) +[![Python Version](https://img.shields.io/badge/python-3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9-blue)](https://www.python.org/downloads/) + + # pyworkforce -Common tools for workforce management, schedule and optimization problems built in top of tools like google's ortools +Common tools for workforce management, schedule and optimization problems built on top of packages like google's ortools and custom modules # Usage: From 1d85f589adcb4149f934f1b9312af8a83425b176 Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Thu, 11 Mar 2021 17:35:57 -0500 Subject: [PATCH 10/13] Independent tests folder per subpackage Fixes coverage report taking example folder --- .coveragerc | 2 ++ .travis.yml | 2 +- {tests => pyworkforce/queuing/tests}/__init__.py | 0 .../test_queing.py => pyworkforce/queuing/tests/test_erlang.py | 0 pyworkforce/shifts/tests/__init__.py | 0 {tests => pyworkforce/shifts/tests}/test_shifts.py | 0 6 files changed, 3 insertions(+), 1 deletion(-) rename {tests => pyworkforce/queuing/tests}/__init__.py (100%) rename tests/test_queing.py => pyworkforce/queuing/tests/test_erlang.py (100%) create mode 100644 pyworkforce/shifts/tests/__init__.py rename {tests => pyworkforce/shifts/tests}/test_shifts.py (100%) diff --git a/.coveragerc b/.coveragerc index ca8eaac..7523c6f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,6 +3,8 @@ omit = ./tests/* ./venv/* ./docs/* + ./examples/* + setup.py [report] precision = 2 show_missing = True \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 9b8b139..e8bb964 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,6 @@ before_install: install: - pip install -r dev-requirements.txt script: - - pytest tests/ --verbose --color=yes --assert=plain --cov-config=.coveragerc --cov=./ + - pytest pyworkforce/ --verbose --color=yes --assert=plain --cov-config=.coveragerc --cov=./ after_success: - codecov diff --git a/tests/__init__.py b/pyworkforce/queuing/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to pyworkforce/queuing/tests/__init__.py diff --git a/tests/test_queing.py b/pyworkforce/queuing/tests/test_erlang.py similarity index 100% rename from tests/test_queing.py rename to pyworkforce/queuing/tests/test_erlang.py diff --git a/pyworkforce/shifts/tests/__init__.py b/pyworkforce/shifts/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_shifts.py b/pyworkforce/shifts/tests/test_shifts.py similarity index 100% rename from tests/test_shifts.py rename to pyworkforce/shifts/tests/test_shifts.py From 6b3104cc41db5a5ea71a32e149e24221e9c50fbb Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Thu, 11 Mar 2021 17:50:00 -0500 Subject: [PATCH 11/13] Fixes coverage report taking example tests folders --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 7523c6f..e5d022e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,7 @@ omit = ./venv/* ./docs/* ./examples/* + */tests/* setup.py [report] precision = 2 From f5b77d7f41d5fdaf4d38fb619418de51d8ca0409 Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:20:56 -0500 Subject: [PATCH 12/13] Fixes wrong variables range verification --- pyworkforce/queuing/erlang.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyworkforce/queuing/erlang.py b/pyworkforce/queuing/erlang.py index 24833de..11bee34 100644 --- a/pyworkforce/queuing/erlang.py +++ b/pyworkforce/queuing/erlang.py @@ -16,17 +16,17 @@ def __init__(self, transactions: float, aht: float, asa: float, :param shrinkage: Percentage of time that an operator unit is not available """ - if transactions < 0: - raise ValueError("transactions can't be smaller than 0") + if transactions <= 0: + raise ValueError("transactions can't be smaller or equals than 0") - if aht < 0: - raise ValueError("aht can't be smaller than 0") + if aht <= 0: + raise ValueError("aht can't be smaller or equals than 0") - if asa < 0: - raise ValueError("asa can't be smaller than 0") + if asa <= 0: + raise ValueError("asa can't be smaller or equals than 0") - if interval < 0: - raise ValueError("interval can't be smaller than 0") + if interval <= 0: + raise ValueError("interval can't be smaller or equals than 0") if shrinkage < 0 or shrinkage >= 1: raise ValueError("shrinkage must be between in the interval [0,1)") @@ -100,7 +100,7 @@ def required_positions(self, service_level: float, max_occupancy: float = 1.0): if service_level < 0 or service_level > 1: raise ValueError("service_level must be between 0 and 1") - if max_occupancy < 0 or service_level > 1: + if max_occupancy < 0 or max_occupancy > 1: raise ValueError("max_occupancy must be between 0 and 1") positions = round(self.intensity + 1) From 75edcbcca384ae28f7a957071eb84c2f0a6910f1 Mon Sep 17 00:00:00 2001 From: rodrigoarenas456 <31422766+rodrigoarenas456@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:21:09 -0500 Subject: [PATCH 13/13] Missing tests on several functions --- pyworkforce/queuing/tests/test_erlang.py | 67 ++++++++++++++++++++++++ pyworkforce/shifts/tests/test_shifts.py | 35 ++++++++++++- pyworkforce/shifts/tests/test_utils.py | 30 +++++++++++ 3 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 pyworkforce/shifts/tests/test_utils.py diff --git a/pyworkforce/queuing/tests/test_erlang.py b/pyworkforce/queuing/tests/test_erlang.py index a26bdfe..2477182 100644 --- a/pyworkforce/queuing/tests/test_erlang.py +++ b/pyworkforce/queuing/tests/test_erlang.py @@ -18,8 +18,75 @@ def test_expected_erlangc_results(): assert round(waiting_probability, 3) == 0.174 +def test_scale_positions_erlangc(): + erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3) + results = erlang.required_positions(service_level=0.8, max_occupancy=0.85) + positions = results['positions'] + service_level = erlang.service_level(positions=positions, scale_positions=True) + occupancy = erlang.achieved_occupancy(positions=positions, scale_positions=True) + waiting_probability = erlang.waiting_probability(positions=positions, scale_positions=True) + + assert positions == 20 + assert round(service_level, 3) == 0.888 + assert round(occupancy, 3) == 0.714 + assert round(waiting_probability, 3) == 0.174 + + +def test_over_occupancy_erlangc(): + erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3) + results = erlang.required_positions(service_level=0.8, max_occupancy=0.7) + raw_positions = results['raw_positions'] + positions = results['positions'] + service_level = erlang.service_level(positions=positions, scale_positions=True) + occupancy = erlang.achieved_occupancy(positions=positions, scale_positions=True) + waiting_probability = erlang.waiting_probability(positions=positions, scale_positions=True) + + assert raw_positions == 15 + assert positions == 22 + assert round(service_level, 3) == 0.941 + assert round(occupancy, 3) == 0.667 + assert round(waiting_probability, 3) == 0.102 + + +def test_wrong_transactions_erlangc(): + with pytest.raises(Exception) as excinfo: + erlang = ErlangC(transactions=-20, asa=0.33, aht=3, interval=30, shrinkage=0.3) + assert str(excinfo.value) == "transactions can't be smaller or equals than 0" + + +def test_wrong_aht_erlangc(): + with pytest.raises(Exception) as excinfo: + erlang = ErlangC(transactions=100, asa=0.33, aht=-5, interval=30, shrinkage=0.3) + assert str(excinfo.value) == "aht can't be smaller or equals than 0" + + +def test_wrong_asa_erlangc(): + with pytest.raises(Exception) as excinfo: + erlang = ErlangC(transactions=100, asa=0, aht=5, interval=30, shrinkage=0.3) + assert str(excinfo.value) == "asa can't be smaller or equals than 0" + + +def test_wrong_interval_erlangc(): + with pytest.raises(Exception) as excinfo: + erlang = ErlangC(transactions=100, asa=10, aht=5, interval=-30, shrinkage=0.3) + assert str(excinfo.value) == "interval can't be smaller or equals than 0" + + +def test_wrong_shrinkage_erlangc(): + with pytest.raises(Exception) as excinfo: + erlang = ErlangC(transactions=100, asa=10, aht=5, interval=30, shrinkage=1) + assert str(excinfo.value) == "shrinkage must be between in the interval [0,1)" + + def test_wrong_service_level_erlangc(): erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3) with pytest.raises(Exception) as excinfo: results = erlang.required_positions(service_level=1.8, max_occupancy=0.85) assert str(excinfo.value) == "service_level must be between 0 and 1" + + +def test_wrong_max_occupancy_erlangc(): + erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3) + with pytest.raises(Exception) as excinfo: + results = erlang.required_positions(service_level=0.8, max_occupancy=1.2) + assert str(excinfo.value) == "max_occupancy must be between 0 and 1" diff --git a/pyworkforce/shifts/tests/test_shifts.py b/pyworkforce/shifts/tests/test_shifts.py index d2cdd7a..8e74bb3 100644 --- a/pyworkforce/shifts/tests/test_shifts.py +++ b/pyworkforce/shifts/tests/test_shifts.py @@ -11,7 +11,7 @@ def test_min_abs_difference_schedule(): "Night": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1], "Mixed": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]} - num_days=2 + num_days = 2 scheduler = MinAbsDifference(num_days=num_days, periods=24, @@ -26,5 +26,36 @@ def test_min_abs_difference_schedule(): assert 'cost' in solution assert 'resources_shifts' in solution assert len(solution['resources_shifts']) == num_days * len(shifts_coverage) - for i in range(num_days*len(shifts_coverage)): + for i in range(num_days * len(shifts_coverage)): assert solution['resources_shifts'][i]['resources'] >= 0 + + +def test_infeasible_min_abs_difference_schedule(): + required_resources = [ + [9, 11, 17, 9, 7, 12, 5, 11, 8, 9, 18, 17, 8, 12, 16, 8, 7, 12, 11, 10, 13, 19, 16, 7], + [13, 13, 12, 15, 18, 20, 13, 16, 17, 8, 13, 11, 6, 19, 11, 20, 19, 17, 10, 13, 14, 23, 16, 8] + ] + shifts_coverage = {"Morning": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "Afternoon": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0], + "Night": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + "Mixed": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]} + + num_days = 2 + + scheduler = MinAbsDifference(num_days=num_days, + periods=24, + shifts_coverage=shifts_coverage, + required_resources=required_resources, + max_period_concurrency=10, + max_shift_concurrency=20) + + solution = scheduler.solve() + + assert solution['status'] == 'INFEASIBLE' + assert 'cost' in solution + assert 'resources_shifts' in solution + assert solution['cost'] == -1 + assert len(solution['resources_shifts']) == 1 + assert solution['resources_shifts'][0]['day'] == -1 + assert solution['resources_shifts'][0]['shift'] == 'Unknown' + assert solution['resources_shifts'][0]['resources'] == -1 diff --git a/pyworkforce/shifts/tests/test_utils.py b/pyworkforce/shifts/tests/test_utils.py new file mode 100644 index 0000000..547fdd7 --- /dev/null +++ b/pyworkforce/shifts/tests/test_utils.py @@ -0,0 +1,30 @@ +import pytest +from pyworkforce.shifts.utils import check_positive_integer, check_positive_float + + +def test_check_positive_integer(): + assert check_positive_integer('my_val', 5) + + +def test_check_non_positive_integers(): + with pytest.raises(Exception) as excinfo: + result = check_positive_integer('my_val', -1) + assert str(excinfo.value) == "my_val must be a positive integer" + + with pytest.raises(Exception) as excinfo: + result = check_positive_integer('my_val2', 5.4) + assert str(excinfo.value) == "my_val2 must be a positive integer" + + +def test_check_positive_float(): + assert check_positive_float('my_val', 2.43) + + +def test_check_non_positive_floats(): + with pytest.raises(Exception) as excinfo: + result = check_positive_float('my_val', -45.3) + assert str(excinfo.value) == "my_val must be a positive float" + + with pytest.raises(Exception) as excinfo: + result = check_positive_float('my_val2', 80) + assert str(excinfo.value) == "my_val2 must be a positive float"