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
rodrigo-arenas authored Mar 22, 2021
2 parents ddfbac7 + 6a40003 commit 2c3a9c8
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
@@ -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
@@ -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:
@@ -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 = [
@@ -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
@@ -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
@@ -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
149 changes: 108 additions & 41 deletions pyworkforce/shifts/shifts_selection.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import numpy as np
import pandas as pd
from ortools.sat.python import cp_model
from pyworkforce.shifts.utils import check_positive_integer, check_positive_float
from pyworkforce.shifts.base import BaseShiftScheduler


class MinAbsDifference:
class MinAbsDifference(BaseShiftScheduler):
def __init__(self, num_days: int,
periods: int,
shifts_coverage: dict,
@@ -14,46 +15,18 @@ def __init__(self, num_days: int,
num_search_workers=4,
*args, **kwargs):
"""
Solves 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)
The "optimal" criteria, is defined as the amount of resources per shifts that minimize the total absolute
difference, between the required resources per period and the actual shifted by the solver
: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
The "optimal" criteria, is defined as the amount of resources per shifts that minimize the total absolute
difference, between the required resources per period and the actual shifted by the solver
"""

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
super().__init__(num_days,
periods,
shifts_coverage,
required_resources,
max_period_concurrency,
max_shift_concurrency,
max_search_time,
num_search_workers)

def solve(self):
sch_model = cp_model.CpModel()
@@ -67,7 +40,7 @@ def solve(self):
# Resources
for d in range(self.num_days):
for s in range(self.num_shifts):
resources[d][s] = sch_model.NewIntVar(0, self.max_shift_concurrency, f'agents_d{d}s{s}')
resources[d][s] = sch_model.NewIntVar(0, self.max_shift_concurrency, f'resources_d{d}s{s}')

for d in range(self.num_days):
for p in range(self.num_periods):
@@ -122,3 +95,97 @@ def solve(self):
"resources_shifts": [{'day': -1, 'shift': 'Unknown', 'resources': -1}]}

return solution


class MinRequiredResources(BaseShiftScheduler):
def __init__(self, num_days: int,
periods: int,
shifts_coverage: dict,
required_resources: list,
max_period_concurrency: int,
max_shift_concurrency: int,
cost_dict: dict = None,
max_search_time: float = 240.0,
num_search_workers: int = 4,
*args, **kwargs):
"""
The "optimal" criteria, is defined as minimum weighted amount of resources (by optional shift cost),
that ensures that there are never less resources shifted that the ones required per period
:param cost_dict: dict of form {shift: cost_value}, where shift must be the same options listed in the
shifts_coverage matrix and they must be all integers
"""

super().__init__(num_days,
periods,
shifts_coverage,
required_resources,
max_period_concurrency,
max_shift_concurrency,
max_search_time,
num_search_workers)

if cost_dict is None:
self.cost_dict = dict.fromkeys(self.shifts, 1)
else:
self.cost_dict = cost_dict

if set(sorted(self.shifts)) == set(sorted(list(self.cost_dict.keys()))):
self.df_cost_matrix = pd.DataFrame.from_records([self.cost_dict])
else:
raise KeyError('cost_dict must have the same keys as shifts_coverage')

def solve(self):
sch_model = cp_model.CpModel()

# Resources: Number of resources assigned in day d to shift s
resources = np.empty(shape=(self.num_days, self.num_shifts), dtype='object')

# Resources
for d in range(self.num_days):
for s in range(self.num_shifts):
resources[d][s] = sch_model.NewIntVar(0, self.max_shift_concurrency, f'resources_d{d}s{s}')

# Constrains

# Total programmed resources in day d and period p, must be greater or equals that required resources in d, p
for d in range(self.num_days):
for p in range(self.num_periods):
sch_model.Add(sum(resources[d][s] * self.shifts_coverage_matrix[s][p]
for s in range(self.num_shifts)) >= self.required_resources[d][p])

# Total programmed resources, must be less or equals to max_period_concurrency, for each day and period
for d in range(self.num_days):
for p in range(self.num_periods):
sch_model.Add(
sum(resources[d][s] * self.shifts_coverage_matrix[s][p]
for s in range(self.num_shifts)) <= self.max_period_concurrency)

# Objective Function: Minimize the total shifted resources
sch_model.Minimize(sum(resources[d][s] * self.df_cost_matrix[self.shifts[s]].item()
for d in range(self.num_days)
for s in range(self.num_shifts)))

self.solver.parameters.max_time_in_seconds = self.max_search_time
self.solver.num_search_workers = self.num_search_workers

self.status = self.solver.Solve(sch_model)

if self.status in [cp_model.OPTIMAL, cp_model.FEASIBLE]:
resources_shifts = []
for d in range(self.num_days):
for s in range(self.num_shifts):
resources_shifts.append({
"day": d,
"shift": self.shifts[s],
"resources": self.solver.Value(resources[d][s])})

solution = {"status": self.solver.StatusName(self.status),
"cost": self.solver.ObjectiveValue(),
"resources_shifts": resources_shifts}
else:
solution = {"status": self.solver.StatusName(self.status),
"cost": -1,
"resources_shifts": [{'day': -1, 'shift': 'Unknown', 'resources': -1}]}

return solution
121 changes: 120 additions & 1 deletion pyworkforce/shifts/tests/test_shifts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pyworkforce.shifts import MinAbsDifference
from pyworkforce.shifts import MinAbsDifference, MinRequiredResources
import pytest


def test_min_abs_difference_schedule():
@@ -59,3 +60,121 @@ def test_infeasible_min_abs_difference_schedule():
assert solution['resources_shifts'][0]['day'] == -1
assert solution['resources_shifts'][0]['shift'] == 'Unknown'
assert solution['resources_shifts'][0]['resources'] == -1


def test_min_required_resources_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 = MinRequiredResources(num_days=num_days,
periods=24,
shifts_coverage=shifts_coverage,
required_resources=required_resources,
max_period_concurrency=25,
max_shift_concurrency=25)

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_cost_min_required_resources_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]}

cost_dict = {"Morning": 8, "Afternoon": 8, "Night": 10, "Mixed": 7}

num_days = 2

scheduler = MinRequiredResources(num_days=num_days,
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()

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_wrong_cost_min_required_resources_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]}

cost_dict = {"Morning": 8, "Night": 10, "Mixed": 7}

num_days = 2
with pytest.raises(Exception) as excinfo:
scheduler = MinRequiredResources(num_days=num_days,
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()
assert str(excinfo.value) == "cost_dict must have the same keys as shifts_coverage"


def test_infeasible_min_required_resources_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 = MinRequiredResources(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'] == '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
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@

setup(
name="pyworkforce",
version="0.2.2",
version="0.3.0",
description="Common tools for workforce management, schedule and optimization problems",
long_description=README,
long_description_content_type="text/markdown",
@@ -28,7 +28,8 @@
packages=find_packages(include=['pyworkforce', 'pyworkforce.*']),
install_requires=[
'numpy>=1.18.1',
'ortools>=7.8.7959'
'ortools>=7.8.7959',
'pandas>=1.0.0'
],
python_requires=">=3.6",
include_package_data=True,

0 comments on commit 2c3a9c8

Please sign in to comment.