-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from rodrigo-arenas/develop
Release 0.2.0
- Loading branch information
Showing
11 changed files
with
334 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
""" | ||
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 MinAbsDifference | ||
|
||
# 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]} | ||
|
||
|
||
scheduler = MinAbsDifference(num_days=2, | ||
periods=24, | ||
shifts_coverage=shifts_coverage, | ||
required_resources=required_resources, | ||
max_period_concurrency=25, | ||
max_shift_concurrency=20) | ||
|
||
solution = scheduler.solve() | ||
print(solution) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from pyworkforce.queuing.erlang import ErlangC | ||
|
||
__all__ = ["ErlangC"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from pyworkforce.shifts.shifts_selection import MinAbsDifference | ||
|
||
__all__ = ["MinAbsDifference"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import numpy as np | ||
from ortools.sat.python import cp_model | ||
from pyworkforce.shifts.utils import check_positive_integer, check_positive_float | ||
|
||
|
||
class MinAbsDifference: | ||
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 = 120.0, | ||
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 # 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 | ||
""" | ||
|
||
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.transposed_shifts_coverage = None | ||
self.status = None | ||
self.solver = None | ||
|
||
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') | ||
# transition resources: Variable to change domain coordinates from min |x-a| | ||
# to min t, s.t t>= x-a and t>= a-x | ||
transition_resources = np.empty(shape=(self.num_days, self.num_periods), 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'agents_d{d}s{s}') | ||
|
||
for d in range(self.num_days): | ||
for p in range(self.num_periods): | ||
transition_resources[d][p] = sch_model.NewIntVar(-self.max_period_concurrency, | ||
self.max_period_concurrency, | ||
f'transition_resources_d{d}p{p}') | ||
|
||
# Constrains | ||
|
||
# transition must be between x-a and a-x | ||
for d in range(self.num_days): | ||
for p in range(self.num_periods): | ||
sch_model.Add(transition_resources[d][p] >= ( | ||
sum(resources[d][s] * self.shifts_coverage_matrix[s][p] for s in range(self.num_shifts)) - | ||
self.required_resources[d][p])) | ||
sch_model.Add(transition_resources[d][p] >= (self.required_resources[d][p] | ||
- sum(resources[d][s] * self.shifts_coverage_matrix[s][p] | ||
for s in range(self.num_shifts)))) | ||
|
||
# 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 absolute value of the difference between required and shifted resources | ||
|
||
sch_model.Minimize( | ||
sum(transition_resources[d][p] for d in range(self.num_days) for p in range(self.num_periods))) | ||
|
||
self.solver = cp_model.CpSolver() | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
|
||
|
||
def check_positive_integer(name, value): | ||
if value <= 0 or not isinstance(value, int): | ||
raise ValueError(f"{name} must be a positive integer") | ||
else: | ||
return True | ||
|
||
|
||
def check_positive_float(name, value): | ||
if value <= 0 or not isinstance(value, float): | ||
raise ValueError(f"{name} must be a positive float") | ||
else: | ||
return True |
Oops, something went wrong.