This commit is contained in:
Ross
2022-01-30 21:23:58 +00:00
parent dcb79a30c2
commit cbe99ea3d8
4 changed files with 217 additions and 74 deletions
+1 -1
View File
@@ -323,7 +323,7 @@ else:
oop,
not_available_to_work=leave_requests,
work_requests=work_requests,
night_site=site_pref,
home_site=site_pref,
previous_shifts=previous_shifts,
pair=pair,
shift_balance_extra=w["shift_balance_extra"],
+50 -45
View File
@@ -43,7 +43,23 @@ SHIFT_BOUNDS = {
class SingleShift(object):
"""Class to hold all details for a shift"""
"""Class to hold all details for a shift
Valid constraints
balance_across_groups (requires block assignment)
postclear(2) / preclear(2)
limit_grade_number:
options: {grade: max_number, grade2: max_number2}
minimum_grade_number:
options: (grade, min_number)
"""
def __init__(
self,
@@ -171,7 +187,6 @@ class RotaBuilder(object):
self.max_weekend_frequency = max_weekend_frequency
self.constraint_options = {
"limit_to_1_st2_on_nights": True,
"ensure_1_st4_plus_on_nights": True,
"ensure_derriford_reg_for_nights": True,
"balance_nights": True,
@@ -722,27 +737,6 @@ class RotaBuilder(object):
# self.model.max_hours_constraint = Constraint(
# [worker.id for worker in self.workers], rule=maxHoursRule)
# Limit to 1 ST2 (or below) on a night shift
def nightShiftMaxSTRule(model, week, shift):
"""Limits to 1 ST2 (or below) per night"""
single_workers = [w for w in self.workers if w.grade < 3]
if not single_workers:
return Constraint.Skip
return (
sum(
model.shift_week_worker_assigned[shift, week, w.id]
for w in single_workers
)
<= 1
)
if self.constraint_options["limit_to_1_st2_on_nights"]:
self.model.night_shifts_max_st_constraint = Constraint(
[week for week in self.weeks],
[shift.name for shift in self.get_shifts_with_constraint("night")],
rule=nightShiftMaxSTRule,
)
# Limit grades
def limitGradesRule(model, week, shift, grade, limit):
workers = [w for w in self.workers if w.grade == grade]
@@ -771,25 +765,34 @@ class RotaBuilder(object):
#name=f"limit_grades_constraint_{shift.name}_{grade}"
))
def nightShiftMinST4Rule(model, week, shift):
single_workers = [w for w in self.workers if w.grade >= 4]
if not single_workers:
return Constraint.Skip
def minimumGradesRule(model, week, shift, grade, limit):
workers = [w for w in self.workers if w.grade >= grade]
if len(workers) < limit:
#return Constraint.Skip
raise ValueError("minimum_grade_number: not enough valid workers")
return (
sum(
model.shift_week_worker_assigned[shift, week, w.id]
for w in single_workers
model.shift_week_worker_assigned[shift.name, week, w.id]
for w in workers
)
>= 1
>= limit
)
if self.constraint_options["ensure_1_st4_plus_on_nights"]:
self.model.night_shifts_min_st4_constraint = Constraint(
for shift in self.get_shifts_with_constraint("minimum_grade_number"):
if not shift.constraint_options["minimum_grade_number"]:
raise ValueError("Constraint option must be defined for 'minimum_grade_number'")
grade, min_required = shift.constraint_options["minimum_grade_number"]
#self.model.limit_grades_constraint = Constraint(
setattr(self.model, f"minimum_grade_number_{shift.name}", Constraint(
[week for week in self.weeks],
[shift.name for shift in self.get_shifts_with_constraint("night")],
rule=nightShiftMinST4Rule,
)
#[shift for shift in self.get_shifts_with_constraint("limit_grade_number")],
[shift],
[grade],
[min_required],
rule=minimumGradesRule,
#name=f"limit_grades_constraint_{shift.name}_{grade}"
))
# def nightShiftDerrifordRule(model, week, shift):
# derriford_workers = [w for w in self.workers if w.night_at_derriford >= 1]
@@ -810,9 +813,9 @@ class RotaBuilder(object):
# rule=nightShiftDerrifordRule,
# )
def presenceAtSiteOvernight(model, week, shift, required_site):
def presenceAtHomeSite(model, week, shift, required_site, required_number):
required_site_workers = [
w for w in self.workers if required_site == w.night_site
w for w in self.workers if required_site == w.home_site
]
return (
@@ -820,18 +823,20 @@ class RotaBuilder(object):
model.shift_week_worker_assigned[shift, week, w.id]
for w in required_site_workers
)
>= 1
>= required_number
)
if self.constraint_options["require_presence_at_site_overnight"]:
for site in self.constraint_options["require_presence_at_site_overnight"]:
self.model.require_presence_at_site_overnight_rule = Constraint(
for shift in self.get_shifts_with_constraint("require_home_site_presence"):
site, required_number = shift.constraint_options["require_home_site_presence"]
#self.model.require_presence_at_site_overnight_rule = Constraint(
setattr(self.model, f"require_home_site_presence_{shift.name}", Constraint(
[week for week in self.weeks],
[shift.name for shift in self.get_shifts_with_constraint("night")],
[shift.name],
[site],
rule=presenceAtSiteOvernight,
)
[required_number],
rule=presenceAtHomeSite,
))
# # Count the number of workers from each site on each night shift
# # As 1 or 0 is optimum we can simply subtract 1 from the number
+160 -22
View File
@@ -23,8 +23,6 @@ class TestDemoRota:
Rota.constraint_options["max_shifts_per_week"] = 5
Rota.constraint_options["max_shifts_per_month"] = 31
Rota.constraint_options["ensure_derriford_reg_for_nights"] = False
Rota.constraint_options["limit_to_1_st2_on_nights"] = False
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = False
Rota.constraint_options["constrain_time_off_after_nights"] = False
Rota.constraint_options["balance_nights_across_site"] = False
Rota.constraint_options["balance_shifts"] = True
@@ -190,7 +188,6 @@ class TestDemoRotaNights:
Rota.constraint_options["max_shifts_per_month"] = 31
Rota.constraint_options["ensure_derriford_reg_for_nights"] = False
Rota.constraint_options["limit_to_1_st2_on_nights"] = False
Rota.constraint_options["ensure_1_st4_plus_on_nights"] = False
Rota.constraint_options["constrain_time_off_after_nights"] = False
Rota.constraint_options["balance_nights_across_site"] = False
Rota.constraint_options["balance_shifts"] = True
@@ -292,18 +289,8 @@ class TestDemoRotaClear:
balance_offset_modifier=1,
max_weekend_frequency=1,
)
#Rota.constraint_options["max_shifts_per_week"] = 5
Rota.constraint_options["max_shifts_per_month"] = 20
#Rota.constraint_options["ensure_derriford_reg_for_nights"] = False
#Rota.constraint_options["limit_to_1_st2_on_nights"] = False
#Rota.constraint_options["ensure_1_st4_plus_on_nights"] = False
#Rota.constraint_options["constrain_time_off_after_nights"] = False
#Rota.constraint_options["balance_nights_across_site"] = False
#Rota.constraint_options["balance_shifts"] = True
#Rota.constraint_options["balance_shifts_over_workers"] = True
#Rota.constraint_options["balance_nights"] = False
#Rota.constraint_options["minimise_shift_diffs"] = False
#Rota.constraint_options["balance_blocks"] = False
worker1 = Worker(Rota, "worker1", "group1", 1)
worker2 = Worker(Rota, "worker2", "group1", 1)
@@ -549,12 +536,12 @@ class TestLimitConstraints:
)
#Rota.constraint_options["max_shifts_per_week"] = 5
#Rota.constraint_options["max_shifts_per_month"] = 20
worker1 = Worker(Rota, "worker1", "group1", 2)
worker2 = Worker(Rota, "worker2", "group1", 2)
worker3 = Worker(Rota, "worker3", "group1", 2)
worker4 = Worker(Rota, "worker4", "group3", 3)
worker5 = Worker(Rota, "worker5", "group3", 3)
worker6 = Worker(Rota, "worker6", "group3", 3)
worker1 = Worker(Rota, "worker1", "group1", 2, home_site="group1")
worker2 = Worker(Rota, "worker2", "group1", 2, home_site="group1")
worker3 = Worker(Rota, "worker3", "group1", 2, home_site="group1")
worker4 = Worker(Rota, "worker4", "group3", 3, home_site="group2")
worker5 = Worker(Rota, "worker5", "group3", 3, home_site="group2")
worker6 = Worker(Rota, "worker6", "group3", 3, home_site="group2")
Rota.add_workers((worker1, worker2, worker3, worker4))
Rota.add_workers((worker5, worker6))
@@ -673,9 +660,160 @@ class TestLimitConstraints:
self.Rota.constraint_options["balance_shifts_quadratic"] = True
self.Rota.build_and_solve(options={"ratio": 0.1})
assert self.Rota.results.solver.termination_condition == "infeasible"
def test_constraint_minimum_grades(self):
self.Rota.shifts = []
self.Rota.add_shifts(
SingleShift(("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40,
workers_required=1,
force_as_block=True,
constraints=[("minimum_grade_number", (3, 1))]
),
)
self.Rota.constraint_options["balance_shifts_quadratic"] = True
self.Rota.build_and_solve(options={"ratio": 0.1})
self.Rota.export_rota_to_html("test9")
assert self.Rota.results.solver.termination_condition == "infeasible"
grade_workers = self.Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = []
for w in grade_workers[grade]:
shift_patterns.append(self.Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
limit = 0
if grade > 2:
limit = 1
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
assert self.Rota.results.solver.status == "ok"
def test_constraint_minimum_grades2(self):
self.Rota.shifts = []
self.Rota.add_shifts(
SingleShift(("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40,
workers_required=4,
force_as_block=True,
constraints=[("minimum_grade_number", (3, 3))]
),
)
self.Rota.constraint_options["balance_shifts_quadratic"] = True
self.Rota.build_and_solve(options={"ratio": 0.1})
self.Rota.export_rota_to_html("test9")
grade_workers = self.Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = []
for w in grade_workers[grade]:
shift_patterns.append(self.Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
limit = 1
if grade > 2:
limit = 3
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
assert self.Rota.results.solver.status == "ok"
def test_constraint_minimum_grades3(self):
self.Rota.shifts = []
self.Rota.add_shifts(
SingleShift(("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40,
workers_required=4,
force_as_block=True,
constraints=[("minimum_grade_number", (3, 0))]
),
)
self.Rota.constraint_options["balance_shifts_quadratic"] = True
self.Rota.build_and_solve(options={"ratio": 0.1})
self.Rota.export_rota_to_html("test9")
assert self.Rota.results.solver.status == "ok"
def test_constraint_minimum_grades_no_valid_worker(self):
self.Rota.shifts = []
self.Rota.add_shifts(
SingleShift(("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40,
workers_required=4,
force_as_block=True,
constraints=[("minimum_grade_number", (4, 1))]
),
)
self.Rota.constraint_options["balance_shifts_quadratic"] = True
with pytest.raises(ValueError):
self.Rota.build_and_solve(options={"ratio": 0.1})
def test_constraint_require_home_site_presence(self):
self.Rota.shifts = []
self.Rota.add_shifts(
SingleShift(("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40,
workers_required=2,
force_as_block=True,
constraints=[("require_home_site_presence", ("group1", 2))]
),
)
self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("test9")
group_workers = self.Rota.get_workers_by_group()
for group in group_workers:
shift_patterns = []
for w in group_workers[group]:
shift_patterns.append(self.Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
limit = 2
if group != "group1":
limit = 0
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
assert self.Rota.results.solver.status == "ok"
assert self.Rota.results.solver.termination_condition == "optimal"
def test_constraint_require_home_site_presence2(self):
self.Rota.shifts = []
self.Rota.add_shifts(
SingleShift(("group1", "group2", "group3"), "weekday_night", 12.5, days, balance_offset=40,
workers_required=3,
force_as_block=True,
constraints=[("require_home_site_presence", ("group2", 2))]
),
)
self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("test9")
group_workers = self.Rota.get_workers_by_group()
for group in group_workers:
shift_patterns = []
for w in group_workers[group]:
shift_patterns.append(self.Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
if group == "group2":
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") >= 2
assert self.Rota.results.solver.status == "ok"
assert self.Rota.results.solver.termination_condition == "optimal"
#class TestNoWorkerRota:
# weeks_to_rota = 10
@@ -693,4 +831,4 @@ class TestLimitConstraints:
if __name__ == "__main__":
t = TestLimitConstraints()
t.test_constraint_limit_grades4()
t.test_constraint_require_home_site_presence()
+4 -4
View File
@@ -20,7 +20,7 @@ class Worker:
not_available_to_work=None,
pref_not_to_work=None,
work_requests=None,
night_site: str = "plymouth", # We set a default proc_site
home_site: str = "plymouth", # We set a default proc_site
previous_shifts: dict = {},
shift_balance_extra: dict = {},
bank_holiday_extra: int = 0,
@@ -44,8 +44,8 @@ class Worker:
self.shift_balance_extra = shift_balance_extra
self.bank_holiday_extra = bank_holiday_extra
self.proc_site = night_site
if night_site == "plymouth" or night_site == "":
self.home_site = home_site
if home_site == "plymouth" or home_site == "":
self.night_at_derriford = 1
else:
self.night_at_derriford = 0
@@ -172,7 +172,7 @@ class Worker:
self.name,
(self.site, self.grade, self.fte_adj),
nwd,
self.proc_site,
self.home_site,
self.night_at_derriford,
)