feat: add exact shifts functionality to Worker class and update related logic in RotaBuilder

This commit is contained in:
Ross
2026-07-08 22:33:33 +01:00
parent 02598cb665
commit 0f31dfe1a9
7 changed files with 398 additions and 102 deletions
+72 -34
View File
@@ -30,6 +30,10 @@ sites = (
"plymouth",
"taunton",
"proc",
"plymouth ir",
"truro ir",
"exeter ir",
"torbay ir",
)
from rota_generator.workers import (
@@ -49,7 +53,7 @@ def main(
time_to_run: int = 60 * 60 * 10,
ratio: float = 0.1,
start_date: datetime.datetime = "2026-09-07",
weeks: int = 26,
weeks: int = 12,
bom: float = 1,
):
rota_start_date = start_date.date()
@@ -65,13 +69,13 @@ def main(
Rota.constraint_options["balance_shifts"] = False
Rota.constraint_options["balance_shifts_quadratic"] = True
Rota.constraint_options["balance_weekends"] = True
#Rota.constraint_options["max_night_frequency"] = 4 # 3
# Rota.constraint_options["max_night_frequency"] = 4 # 3
Rota.constraint_options["max_night_frequency_week_exclusions"] = []
#Rota.constraint_options["max_weekend_frequency"] = 3
# Rota.constraint_options["max_weekend_frequency"] = 3
#Rota.constraint_options["max_days_per_week_block"] = [(8,4)]
# Rota.constraint_options["max_days_per_week_block"] = [(8,4)]
#wr = [
# wr = [
# WorkerRequirement(
# end_date=datetime.datetime.strptime("2025-06-02", "%Y-%m-%d").date(),
# number=3,
@@ -80,7 +84,7 @@ def main(
# start_date=datetime.datetime.strptime("2025-06-02", "%Y-%m-%d").date(),
# number=4,
# ),
#]
# ]
Rota.add_shifts(
SingleShift(
@@ -89,6 +93,7 @@ def main(
"exeter twilights and weekends",
"exeter no nights",
"exeter twilights",
"exeter ir",
),
name="exeter_twilight",
length=12.5,
@@ -104,6 +109,7 @@ def main(
"truro twilights and weekends",
"truro twilights and weekend nights",
"truro twilights, weekends and weekend nights",
"truro ir",
),
name="truro_twilight",
length=12.5,
@@ -112,7 +118,12 @@ def main(
constraints=[MaxShiftsPerWeekConstraint(max_shifts=1)],
),
SingleShift(
sites=("torbay", "torbay twilights", "torbay twilights and weekends"),
sites=(
"torbay",
"torbay twilights",
"torbay twilights and weekends",
"torbay ir",
),
name="torbay_twilight",
length=12.5,
days=days[:4],
@@ -125,6 +136,7 @@ def main(
"plymouth twilights",
"plymouth twilights and weekends",
"plymouth twilights, weekends and weekend nights",
"plymouth ir",
),
name="plymouth_twilight",
length=12.5,
@@ -139,12 +151,12 @@ def main(
"exeter twilights and weekends",
"exeter no nights",
"exeter weekends",
#"exeter twilights",
# "exeter twilights",
),
name="weekend_exeter",
length=12.5,
days=days[5:],
#balance_offset=2,
# balance_offset=2,
rota_on_nwds=True,
force_as_block=True,
constraints=[PostShiftConstraint(days=2), PreShiftConstraint(days=2)],
@@ -161,10 +173,10 @@ def main(
name="weekend_truro",
length=12.5,
days=days[4:],
#balance_offset=3,
# balance_offset=3,
rota_on_nwds=True,
force_as_block=True,
#assign_as_block=True,
# assign_as_block=True,
constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
# force_as_block_unless_nwd=True
),
@@ -173,7 +185,7 @@ def main(
name="weekend_torbay",
length=12.5,
days=days[4:],
#balance_offset=2,
# balance_offset=2,
rota_on_nwds=True,
force_as_block=True,
constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
@@ -189,7 +201,7 @@ def main(
name="weekend_plymouth1",
length=8,
days=days[5:],
#balance_offset=2.9,
# balance_offset=2.9,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
@@ -204,7 +216,7 @@ def main(
name="weekend_plymouth2",
length=8,
days=days[5:],
#balance_offset=2.9,
# balance_offset=2.9,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
@@ -216,6 +228,7 @@ def main(
"plymouth twilights",
"plymouth twilights and weekends",
"plymouth twilights, weekends and weekend nights",
"plymouth ir",
),
name="plymouth_bank_holidays",
length=8,
@@ -227,11 +240,13 @@ def main(
bank_holidays_only=True,
),
SingleShift(
sites=sites,
sites=[
*sites,
],
name="night_weekday",
length=12.25,
days=days[:4],
#balance_offset=3.9,
# balance_offset=3.9,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=4,
@@ -245,7 +260,7 @@ def main(
LimitGradeNumberConstraint(grade=2, max_number=2),
MinimumGradeNumberConstraint(grade=4, min_number=1),
],
#end_date=datetime.datetime.strptime("2025-06-01", "%Y-%m-%d").date(),
# end_date=datetime.datetime.strptime("2025-06-01", "%Y-%m-%d").date(),
),
SingleShift(
sites=[
@@ -258,7 +273,7 @@ def main(
name="night_weekend",
length=12.25,
days=days[4:],
#balance_offset=2.9,
# balance_offset=2.9,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=4,
@@ -272,15 +287,19 @@ def main(
LimitGradeNumberConstraint(grade=2, max_number=2),
MinimumGradeNumberConstraint(grade=4, min_number=1),
],
#end_date=datetime.datetime.strptime("2025-06-01", "%Y-%m-%d").date(),
# end_date=datetime.datetime.strptime("2025-06-01", "%Y-%m-%d").date(),
),
)
# Prevent shifts for ST2s for 2 weeks
#Rota.add_grade_constraint_by_week([2], [1, 2], [shift.name for shift in Rota.shifts])
# Rota.add_grade_constraint_by_week([2], [1, 2], [shift.name for shift in Rota.shifts])
Rota.add_min_summed_grade_by_shifts_per_day_constraint(["weekend_plymouth1", "weekend_plymouth2"], 6)
Rota.add_min_summed_grade_by_shifts_per_day_constraint(["plymouth_twilight", "plymouth_bank_holidays"], 6)
Rota.add_min_summed_grade_by_shifts_per_day_constraint(
["weekend_plymouth1", "weekend_plymouth2"], 6
)
Rota.add_min_summed_grade_by_shifts_per_day_constraint(
["plymouth_twilight", "plymouth_bank_holidays"], 6
)
Rota.add_min_summed_grade_by_shifts_per_day_constraint(["night_weekday"], 12)
Rota.add_min_summed_grade_by_shifts_per_day_constraint(["night_weekend"], 12)
@@ -327,7 +346,7 @@ def main(
after_count = len(filtered)
if after_count != before_count:
print(
f"Removed {before_count-after_count} leave entries for '{name}' between {_from} and {_to}"
f"Removed {before_count - after_count} leave entries for '{name}' between {_from} and {_to}"
)
w["leave"] = filtered
@@ -375,9 +394,9 @@ def main(
nwd_end_date = Rota.rota_end_date
if "[" in i:
#print("Split nwd", worker_name, i)
# print("Split nwd", worker_name, i)
a, b = i.split("[")[1][:-1].split("-")
#print(f"{a=} {b=}")
# print(f"{a=} {b=}")
nwd_start_date = datetime.datetime.strptime(
a, "%d/%m/%Y"
).date()
@@ -406,7 +425,9 @@ def main(
elif "to" in raw:
s, e = raw.split("to", 1)
else:
raise ValueError(f"Cannot parse OOP dates: '{oop}' for {worker}")
raise ValueError(
f"Cannot parse OOP dates: '{oop}' for {worker}"
)
s = s.strip().strip("()[]\"' ")
e = e.strip().strip("()[]\"' ")
@@ -416,7 +437,9 @@ def main(
try:
start_dt = datetime.datetime.strptime(s, fmt).date()
end_dt = datetime.datetime.strptime(e, fmt).date()
formatted_oops.append({"start_date": start_dt, "end_date": end_dt})
formatted_oops.append(
{"start_date": start_dt, "end_date": end_dt}
)
parsed = True
break
except ValueError:
@@ -425,13 +448,15 @@ def main(
if not parsed:
print("WORKER", worker)
print("DATES", s, e)
raise ValueError(f"Cannot parse OOP dates: '{oop}' for {worker}")
raise ValueError(
f"Cannot parse OOP dates: '{oop}' for {worker}"
)
oop = formatted_oops
else:
oop = []
#print(f"{worker} OOP {oop}")
# print(f"{worker} OOP {oop}")
previous_shifts = {}
# if twi:
@@ -465,16 +490,29 @@ def main(
shift_fte_overrides = {}
#if worker_name == "Hadi Mohamed":
exact_shifts = {}
if worker in ["hannah.lewis29@nhs.net", "shruti.bodapati@nhs.net", "a.abouelatta@nnhs.net"]:
exact_shifts = {
"night_weekday": 4,
"night_weekend": 3,
}
elif worker in ["g.abdelhalim@nhs.net", ""]:
exact_shifts = {
"night_weekend": 3,
"night_weekday": 0,
}
# if worker_name == "Hadi Mohamed":
# shift_fte_overrides = {
# "weekend_exeter": 50,
# }
#elif worker_name == "Joel Lim":
# elif worker_name == "Joel Lim":
# shift_fte_overrides = {
# "plymouth_twilight": 100,
# "weekend_exeter": 50,
# }
#if worker_name == "Nang Thiriphoo":
# if worker_name == "Nang Thiriphoo":
# shift_fte_overrides = {
# "weekend_exeter": 40,
# }
@@ -497,9 +535,10 @@ def main(
shift_balance_extra=w["shift_balance_extra"],
bank_holiday_extra=w["bank_holiday_extra"],
shift_fte_overrides=shift_fte_overrides,
exact_shifts=exact_shifts,
)
#print(w)
# print(w)
Rota.add_worker(w)
leave.load_academy(Rota)
@@ -507,7 +546,6 @@ def main(
# Rota.build_workers()
# Rota.build_model()
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
# solver_options = {"seconds": time_to_run, "threads": 10}