.
This commit is contained in:
+1
-1
@@ -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"],
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user