Skip to content

Commit

Permalink
Merge pull request #12 from rodrigo-arenas/develop
Browse files Browse the repository at this point in the history
Release 0.2.1
  • Loading branch information
rodrigo-arenas committed Mar 11, 2021
2 parents 1cb3eba + 07b42a2 commit 790a9c6
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 66 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ omit =
./tests/*
./venv/*
./docs/*
./examples/*
*/tests/*
setup.py
[report]
precision = 2
show_missing = True
15 changes: 15 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
language: python
python:
- 3.6
- 3.7
- 3.8
- 3.9
before_install:
- python --version
- pip install -U pip
install:
- pip install -r dev-requirements.txt
script:
- pytest pyworkforce/ --verbose --color=yes --assert=plain --cov-config=.coveragerc --cov=./
after_success:
- codecov
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +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:
Expand Down
4 changes: 3 additions & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pytest==6.2.2
codecov==2.1.11
pytest-cov==2.11.1
twine==2.4.7
twine==3.3.0
numpy>=1.18.1
ortools>=7.8.7959
18 changes: 9 additions & 9 deletions pyworkforce/queuing/erlang.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down Expand Up @@ -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)
Expand Down
File renamed without changes.
92 changes: 92 additions & 0 deletions pyworkforce/queuing/tests/test_erlang.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import pytest
from pyworkforce.queuing import ErlangC


def test_expected_erlangc_results():
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)
raw_positions = results['raw_positions']
positions = results['positions']
service_level = results['service_level']
occupancy = results['occupancy']
waiting_probability = results['waiting_probability']

assert raw_positions == 14
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_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"
Empty file.
61 changes: 61 additions & 0 deletions pyworkforce/shifts/tests/test_shifts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from pyworkforce.shifts import MinAbsDifference


def test_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=25,
max_shift_concurrency=20)

solution = scheduler.solve()

assert solution['status'] == 'OPTIMAL'
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)):
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
30 changes: 30 additions & 0 deletions pyworkforce/shifts/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@
'numpy>=1.18.1',
'ortools>=7.8.7959'
],
python_requires=">=3.6",
include_package_data=True,
)
25 changes: 0 additions & 25 deletions tests/test_queing.py

This file was deleted.

30 changes: 0 additions & 30 deletions tests/test_shifts.py

This file was deleted.

0 comments on commit 790a9c6

Please sign in to comment.