Skip to content

Commit

Permalink
Merge pull request #21 from rodrigo-arenas/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
rodrigo-arenas authored Mar 22, 2021
2 parents ddfbac7 + 6a40003 commit 2c3a9c8
Show file tree
Hide file tree
Showing 10 changed files with 388 additions and 75 deletions.
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ python:
- 3.9
os:
- linux
env:
global:
- COV_THRESHOLD=95
before_install:
- python --version
- pip install -U pip
install:
- pip install -r dev-requirements.txt
script:
- pytest pyworkforce/ --verbose --color=yes --assert=plain --cov-fail-under=95 --cov-config=.coveragerc --cov=./
- pytest pyworkforce/ --verbose --color=yes --assert=plain --cov-fail-under=$COV_THRESHOLD --cov-config=.coveragerc --cov=./
after_success:
- codecov
83 changes: 57 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,30 @@ and custom modules
pyworkforce currently includes:

[Queue Systems](./pyworkforce/queuing):
- **queing.ErlangC:** Find the number of positions required to attend incoming traffic to a constant rate and infinite queue length and no dropout.
- **queing.ErlangC:** Find the number of positions required to attend incoming traffic to a constant rate, infinite queue length and no dropout.

[Shifts](./pyworkforce/shifts):
- **shifts.MinAbsDifference:** Find the number of resources to schedule in a shift, based in the number of required positions per time interval (found for example using [queing.ErlangC](./pyworkforce/queuing/erlang.py)), maximum capacity restrictions and static shifts coverage.<br>
This module finds the "optimal" assignation by minimizing the total absolute differences between required resources per interval, against the scheduled resources found by the solver.

It finds the number of resources to schedule in a shift, based in the number of required positions per time interval (found for example using [queing.ErlangC](./pyworkforce/queuing/erlang.py)), maximum capacity restrictions and static shifts coverage.<br>
- **shifts.MinAbsDifference:** This module finds the "optimal" assignation by minimizing the total absolute differences between required resources per interval, against the scheduled resources found by the solver.
- **shifts.MinRequiredResources**: This module finds the "optimal" assignation by minimizing the total weighted amount of scheduled resources (optionally weighted by shift cost), it ensures that in all intervals, there are
never less resources shifted that the ones required per period.


# Usage:
For complete list and details of examples go to the
[examples folder](https://github.com/rodrigo-arenas/pyworkforce/tree/develop/examples)
Install pyworkforce

install pyworkforce
It's advised to install pyworkforce using a virtual env, inside the env use:

```
pip install pyworkforce
```

If you are having troubles with or-tools installation, check the [or-tools guide](https://github.com/google/or-tools#installation)

For complete list and details of examples go to the
[examples folder](https://github.com/rodrigo-arenas/pyworkforce/tree/develop/examples)

### Queue systems:

#### Example:
Expand All @@ -57,7 +62,7 @@ Output:
#### Example:

```python
from pyworkforce.shifts import MinAbsDifference
from pyworkforce.shifts import MinAbsDifference, MinRequiredResources

# Rows are the days, each entry of a row, is number of positions required at an hour of the day (24).
required_resources = [
Expand All @@ -72,27 +77,53 @@ shifts_coverage = {"Morning": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0
"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]}


scheduler = MinAbsDifference(num_days=2,
periods=24,
shifts_coverage=shifts_coverage,
required_resources=required_resources,
max_period_concurrency=25,
max_shift_concurrency=20)
# Method One
difference_scheduler = MinAbsDifference(num_days=2,
periods=24,
shifts_coverage=shifts_coverage,
required_resources=required_resources,
max_period_concurrency=27,
max_shift_concurrency=25)

difference_solution = difference_scheduler.solve()

# Method Two

requirements_scheduler = MinRequiredResources(num_days=2,
periods=24,
shifts_coverage=shifts_coverage,
required_resources=required_resources,
max_period_concurrency=27,
max_shift_concurrency=25)

requirements_solution = requirements_scheduler.solve()

print("difference_solution :", difference_solution)

solution = scheduler.solve()
print("solution :", solution)
print("requirements_solution :", requirements_solution)
```
Output:
```
>> solution: {'status': 'OPTIMAL',
'cost': 157.0,
'resources_shifts': [{'day': 0, 'shift': 'Morning', 'resources': 8},
{'day': 0, 'shift': 'Afternoon', 'resources': 11},
{'day': 0, 'shift': 'Night', 'resources': 9},
{'day': 0, 'shift': 'Mixed', 'resources': 1},
{'day': 1, 'shift': 'Morning', 'resources': 13},
{'day': 1, 'shift': 'Afternoon', 'resources': 17},
{'day': 1, 'shift': 'Night', 'resources': 13},
{'day': 1, 'shift': 'Mixed', 'resources': 0}]
}
>> difference_solution: {'status': 'OPTIMAL',
'cost': 157.0,
'resources_shifts': [{'day': 0, 'shift': 'Morning', 'resources': 8},
{'day': 0, 'shift': 'Afternoon', 'resources': 11},
{'day': 0, 'shift': 'Night', 'resources': 9},
{'day': 0, 'shift': 'Mixed', 'resources': 1},
{'day': 1, 'shift': 'Morning', 'resources': 13},
{'day': 1, 'shift': 'Afternoon', 'resources': 17},
{'day': 1, 'shift': 'Night', 'resources': 13},
{'day': 1, 'shift': 'Mixed', 'resources': 0}]
}
>> requirements_solution: {'status': 'OPTIMAL',
'cost': 113.0,
'resources_shifts': [{'day': 0, 'shift': 'Morning', 'resources': 15},
{'day': 0, 'shift': 'Afternoon', 'resources': 13},
{'day': 0, 'shift': 'Night', 'resources': 19},
{'day': 0, 'shift': 'Mixed', 'resources': 3},
{'day': 1, 'shift': 'Morning', 'resources': 20},
{'day': 1, 'shift': 'Afternoon', 'resources': 20},
{'day': 1, 'shift': 'Night', 'resources': 23},
{'day': 1, 'shift': 'Mixed', 'resources': 0}]}
```
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pytest-cov==2.11.1
twine==3.3.0
numpy>=1.18.1
ortools>=7.8.7959
pandas>=1.0.0
4 changes: 2 additions & 2 deletions examples/shifts/min_abs_difference.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
periods=24,
shifts_coverage=shifts_coverage,
required_resources=required_resources,
max_period_concurrency=25,
max_shift_concurrency=20)
max_period_concurrency=27,
max_shift_concurrency=25)

solution = scheduler.solve()
print(solution)
40 changes: 40 additions & 0 deletions examples/shifts/min_required_resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
Requirement: Find the number of workers needed to schedule per shift in a production plant for the next 2 days with the
following conditions:
* There is a number of required persons per hour and day given in the matrix "required_resources"
* There are 4 available shifts called "Morning", "Afternoon", "Night", "Mixed"; their start and end hour is
determined in the dictionary "shifts_coverage", 1 meaning the shift is active at that hour, 0 otherwise
* The number of required workers per day and period (hour) is determined in the matrix "required_resources"
* The maximum number of workers that can be shifted simultaneously at any hour is 25, due plat capacity restrictions
* The maximum number of workers that can be shifted in a same shift, is 20
"""

from pyworkforce.shifts import MinRequiredResources

# Columns are an hour of the day, rows are the days
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]
]

# Each entry of a shift, is an hour of the day (24 columns)
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]}


# The cost of shifting a resource if each shift, if present, solver will minimize the total cost
cost_dict = {"Morning": 8, "Afternoon": 8, "Night": 10, "Mixed": 7}

scheduler = MinRequiredResources(num_days=2,
periods=24,
shifts_coverage=shifts_coverage,
required_resources=required_resources,
cost_dict=cost_dict,
max_period_concurrency=25,
max_shift_concurrency=25)

solution = scheduler.solve()

print(solution)
4 changes: 2 additions & 2 deletions pyworkforce/shifts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from pyworkforce.shifts.shifts_selection import MinAbsDifference
from pyworkforce.shifts.shifts_selection import MinAbsDifference, MinRequiredResources

__all__ = ["MinAbsDifference"]
__all__ = ["MinAbsDifference", "MinRequiredResources"]
51 changes: 51 additions & 0 deletions pyworkforce/shifts/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from ortools.sat.python import cp_model
from pyworkforce.shifts.utils import check_positive_integer, check_positive_float


class BaseShiftScheduler:
def __init__(self, num_days: int,
periods: int,
shifts_coverage: dict,
required_resources: list,
max_period_concurrency: int,
max_shift_concurrency: int,
max_search_time: float = 240.0,
num_search_workers=4):

"""
Base class to solve the following schedule problem:
Its required to find the optimal number of resources (agents, operators, doctors, etc) to allocate
in a shift, based on a pre-defined requirement of number of resources per period of the day (periods of hours,
half-hour, etc)
:param num_days: Number of days needed to schedule
:param periods: Number of working periods in a day
:param shifts_coverage: dict with structure {"shift_name": "shift_array"} where "shift_array" is an array of size [periods] (p), 1 if shift covers period p, 0 otherwise
:param max_period_concurrency: Maximum resources allowed to shift in any period and day
:param required_resources: Array of size [days, periods]
:param max_shift_concurrency: Number of maximum allowed resources in a same shift
:param max_search_time: Maximum time in seconds to search for a solution
:param num_search_workers: Number of workers to search a solution
"""

is_valid_num_days = check_positive_integer("num_days", num_days)
is_valid_periods = check_positive_integer("periods", periods)
is_valid_max_period_concurrency = check_positive_integer("max_period_concurrency", max_period_concurrency)
is_valid_max_shift_concurrency = check_positive_integer("max_shift_concurrency", max_shift_concurrency)
is_valid_max_search_time = check_positive_float("max_search_time", max_search_time)
is_valid_num_search_workers = check_positive_integer("num_search_workers", num_search_workers)

self.num_days = num_days
self.shifts = list(shifts_coverage.keys())
self.num_shifts = len(self.shifts)
self.num_periods = periods
self.shifts_coverage_matrix = list(shifts_coverage.values())
self.max_shift_concurrency = max_shift_concurrency
self.max_period_concurrency = max_period_concurrency
self.required_resources = required_resources
self.max_search_time = max_search_time
self.num_search_workers = num_search_workers
self.solver = cp_model.CpSolver()
self.transposed_shifts_coverage = None
self.status = None
Loading

0 comments on commit 2c3a9c8

Please sign in to comment.