more updates

This commit is contained in:
Ross
2022-07-14 12:12:50 +01:00
parent f79e32ba1d
commit 8f8e5a2a28
6 changed files with 338 additions and 195 deletions
+137 -135
View File
@@ -13,7 +13,6 @@ sites = (
"plymouth", "plymouth",
"taunton", "taunton",
"proc", "proc",
"truro no proc",
) )
from rota.workers import ( from rota.workers import (
@@ -29,154 +28,156 @@ from rota.workers import (
suspend_on_finish = False suspend_on_finish = False
solve = True solve = True
time_to_run = 258500 time_to_run = 258500
time_to_run = 36000
time_to_run = 200
# allow = 5000 # allow = 5000
ratio = 0.5 ratio = 0.05
start_date = datetime.date(2022, 9, 5) start_date = datetime.date(2022, 9, 5)
#start_date = datetime.date(2022, 10, 10) #start_date = datetime.date(2022, 10, 10)
Rota = RotaBuilder(start_date, weeks_to_rota=12) Rota = RotaBuilder(start_date, weeks_to_rota=26)
Rota.constraint_options["balance_weekends"] = True Rota.constraint_options["balance_weekends"] = True
Rota.constraint_options["max_night_frequency"] = 4 Rota.constraint_options["max_night_frequency"] = 3
Rota.constraint_options["max_weekend_frequency"] = 4 Rota.constraint_options["max_weekend_frequency"] = 3
# Rota.constraint_options["avoid_st2_first_month"] = True # Rota.constraint_options["avoid_st2_first_month"] = True
Rota.add_shifts( Rota.add_shifts(
SingleShift( #SingleShift(
sites=("exeter", "exeter twilights and weekends"), # sites=("exeter", "exeter twilights and weekends"),
name="exeter_twilight", # name="exeter_twilight",
length=12.5, # length=12.5,
days=days[:5], # days=days[:5],
balance_offset=3, # balance_offset=3,
constraint=[{"name": "max_shifts_per_week", "options": 2, }], # constraint=[{"name": "max_shifts_per_week", "options": 2, }],
), #),
SingleShift( #SingleShift(
sites=("truro", "truro twilights"), # sites=("truro", "truro twilights"),
name="truro_twilight", # name="truro_twilight",
length=12.5, # length=12.5,
days=days[:4], # days=days[:4],
balance_offset=3, # balance_offset=3,
), #),
SingleShift( #SingleShift(
sites=("torbay", "torbay twilights"), # sites=("torbay", "torbay twilights"),
name="torbay_twilight", # name="torbay_twilight",
length=12.5, # length=12.5,
days=days[:4], # days=days[:4],
balance_offset=5, # balance_offset=5,
assign_as_block=True, # assign_as_block=True,
), #),
SingleShift( SingleShift(
sites=("plymouth", "plymouth twilights"), sites=("plymouth", "plymouth twilights"),
name="plymouth_twilight", name="plymouth_twilight",
length=12.5, length=12.5,
days=days[:5], days=days[:5],
balance_offset=3, balance_offset=2,
workers_required=1, workers_required=1,
constraint=[{"name": "max_shifts_per_week", "options": 2}], constraint=[{"name": "max_shifts_per_week", "options": 2}],
), ),
SingleShift( #SingleShift(
sites=("exeter", "exeter twilights and weekends"), # sites=("exeter", "exeter twilights and weekends"),
name="weekend_exeter", # name="weekend_exeter",
length=12.5, # length=12.5,
days=days[5:], # days=days[5:],
balance_offset=4, # balance_offset=4,
rota_on_nwds=True, # rota_on_nwds=True,
force_as_block=True, # force_as_block=True,
constraint=[{"name": "post", "options": 2}, {"name": "pre", "options": 5}], # constraint=[{"name": "post", "options": 2}, {"name": "pre", "options": 5}],
#constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},], # #constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
), #),
SingleShift( #SingleShift(
sites=("truro",), # sites=("truro",),
name="weekend_truro", # name="weekend_truro",
length=12.5, # length=12.5,
days=days[4:], # days=days[4:],
balance_offset=4, # balance_offset=4,
rota_on_nwds=True, # rota_on_nwds=True,
force_as_block=True, # force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
# force_as_block_unless_nwd=True # # force_as_block_unless_nwd=True
), #),
SingleShift( #SingleShift(
sites=("torbay",), # sites=("torbay",),
name="weekend_torbay", # name="weekend_torbay",
length=12.5, # length=12.5,
days=days[4:], # days=days[4:],
balance_offset=4, # balance_offset=4,
rota_on_nwds=True, # rota_on_nwds=True,
force_as_block=True, # force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
# force_as_block_unless_nwd=True # # force_as_block_unless_nwd=True
), #),
SingleShift( #SingleShift(
sites=("plymouth",), # sites=("plymouth",),
name="weekend_plymouth1", # name="weekend_plymouth1",
length=8, # length=8,
days=days[5:], # days=days[5:],
balance_offset=4, # balance_offset=4,
workers_required=1, # workers_required=1,
rota_on_nwds=True, # rota_on_nwds=True,
force_as_block=True, # force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
), #),
SingleShift( #SingleShift(
sites=("plymouth",), # sites=("plymouth",),
name="weekend_plymouth2", # name="weekend_plymouth2",
length=8, # length=8,
days=days[5:], # days=days[5:],
balance_offset=4, # balance_offset=4,
workers_required=1, # workers_required=1,
rota_on_nwds=True, # rota_on_nwds=True,
force_as_block=True, # force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
), #),
SingleShift( #SingleShift(
sites=("plymouth", "plymouth twilights"), # sites=("plymouth", "plymouth twilights"),
name="plymouth_bank_holidays", # name="plymouth_bank_holidays",
length=8, # length=8,
days=days[5:], # days=days[5:],
balance_offset=2, # balance_offset=2,
workers_required=1, # workers_required=1,
rota_on_nwds=True, # rota_on_nwds=True,
force_as_block=False, # force_as_block=False,
bank_holidays_only=True, # bank_holidays_only=True,
), #),
SingleShift( #SingleShift(
sites=(sites[:-1]), # sites=(sites),
name="night_weekday", # name="night_weekday",
length=12.25, # length=12.25,
days=days[:4], # days=days[:4],
balance_offset=4.9, # balance_offset=4.9,
balance_weighting=1, # balance_weighting=1,
# hard_constrain_shift=False, # # hard_constrain_shift=False,
workers_required=3, # workers_required=3,
force_as_block=True, # force_as_block=True,
rota_on_nwds=True, # rota_on_nwds=True,
constraint=[ # constraint=[
{"name": "night"}, # {"name": "night"},
{"name": "pre", "options": 5}, # {"name": "pre", "options": 5},
{"name": "post", "options": 5}, # {"name": "post", "options": 5},
{"name": "require_remote_site_presence_week", "options": ("plymouth", 1)}, # {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)},
], # ],
), #),
SingleShift( #SingleShift(
sites=(sites[:-1]), # sites=(sites),
name="night_weekend", # name="night_weekend",
length=12.25, # length=12.25,
days=days[4:], # days=days[4:],
balance_offset=3.9, # balance_offset=3.9,
balance_weighting=1, # balance_weighting=1,
# hard_constrain_shift=False, # # hard_constrain_shift=False,
workers_required=3, # workers_required=3,
force_as_block=True, # force_as_block=True,
rota_on_nwds=True, # rota_on_nwds=True,
constraint=[ # constraint=[
{"name": "night"}, # {"name": "night"},
{"name": "pre", "options": 5}, # {"name": "pre", "options": 5},
{"name": "post", "options": 5}, # {"name": "post", "options": 5},
{"name": "require_remote_site_presence_week", "options": ("plymouth", 1)}, # {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)},
], # ],
), #),
) )
Rota.add_grade_constraint_by_week([2], [1, 2, 3, 4], ["night_weekday", "night_weekend"]) Rota.add_grade_constraint_by_week([2], [1, 2, 3, 4], ["night_weekday", "night_weekend"])
@@ -283,13 +284,14 @@ if load_leave:
) )
) )
Rota.build_workers() #Rota.build_workers()
Rota.build_model() #Rota.build_model()
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 6} solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
start_time = time.time() start_time = time.time()
Rota.solve_model(options=solver_options) Rota.solve_shifts_individually(solver_options)
#Rota.solve_model(options=solver_options)
end_time = time.time() end_time = time.time()
print(f"Time taken {end_time-start_time}") print(f"Time taken {end_time-start_time}")
+2
View File
@@ -0,0 +1,2 @@
from rich.console import Console
console = Console()
+65 -22
View File
@@ -23,6 +23,8 @@ from collections import defaultdict
from io import StringIO from io import StringIO
import sys import sys
from rich.pretty import pprint
ShiftName = str ShiftName = str
DayStr = str DayStr = str
WeekInt = int WeekInt = int
@@ -248,6 +250,8 @@ class RotaBuilder(object):
self.results = None self.results = None
self.ignore_valid_shifts = False
def solve_model( def solve_model(
self, solver: str = "cbc", use_neos: bool = False, options: dict = {} self, solver: str = "cbc", use_neos: bool = False, options: dict = {}
): ):
@@ -279,6 +283,25 @@ class RotaBuilder(object):
if not results.solver.status: if not results.solver.status:
sys.exit(0) sys.exit(0)
def solve_shifts_individually(self, options, folder="individual_shifts"):
shifts = self.shifts
outcomes = {}
for shift in shifts:
self.ignore_valid_shifts = True
self.clear_shifts()
self.add_shift(shift)
self.build_and_solve(options)
self.export_rota_to_html(filename=shift.name, folder=folder)
outcomes[shift.name] = (self.results.solver.status, self.results.solver.termination_condition)
pprint(outcomes)
pprint(outcomes)
def build_and_solve( def build_and_solve(
self, options={"ratio": 0.1, "seconds": 1000, "threads": 10}, solve=True self, options={"ratio": 0.1, "seconds": 1000, "threads": 10}, solve=True
): ):
@@ -640,7 +663,7 @@ class RotaBuilder(object):
# Validate shifts are valid # Validate shifts are valid
# work_request_sets = set([i[3] for i in self.work_requests]) # work_request_sets = set([i[3] for i in self.work_requests])
invalid_shifts = work_request_sets - set(self.get_shift_names()) invalid_shifts = work_request_sets - set(self.get_shift_names())
if invalid_shifts: if invalid_shifts and not self.ignore_valid_shifts:
raise InvalidShift( raise InvalidShift(
f"Invalid worker shift request [shift(s): ${invalid_shifts}]" f"Invalid worker shift request [shift(s): ${invalid_shifts}]"
) )
@@ -1036,22 +1059,30 @@ class RotaBuilder(object):
for week_blocks in self.get_week_block_iterator(4): for week_blocks in self.get_week_block_iterator(4):
# Prevent more than n number shifts per 4 weeks # Prevent more than n number shifts per 4 weeks
self.model.constraints.add( try:
self.constraint_options["max_shifts_per_month"] self.model.constraints.add(
>= sum( self.constraint_options["max_shifts_per_month"]
self.model.works[worker.id, week, day, shiftname] >= sum(
for week, day, shiftname in self.get_all_shiftname_combinations() self.model.works[worker.id, week, day, shiftname]
if week in week_blocks for week, day, shiftname in self.get_all_shiftname_combinations()
if week in week_blocks
)
) )
) except ValueError:
# Occurs if there are no shifts within a defined block
# TODO: test if this breaks (and we should check rathar than except)
pass
for constraint_dict in self.constraint_options["avoid_shifts_by_grades"]: for constraint_dict in self.constraint_options["avoid_shifts_by_grades"]:
if invalid_shifts := set(constraint_dict["shifts"]).difference( if invalid_shifts := set(constraint_dict["shifts"]).difference(
set(self.get_shift_names()) set(self.get_shift_names())
): ):
raise ValueError( if self.ignore_valid_shifts:
f"avoid shifts by grade constraint contains a non existent shift: {invalid_shifts}" continue # Skip if we don't want to raise an error
) else:
raise InvalidShift(
f"avoid shifts by grade constraint contains a non existent shift: {invalid_shifts}"
)
if invalid_grades := set(constraint_dict["grades"]).difference( if invalid_grades := set(constraint_dict["grades"]).difference(
set(self.get_worker_grades()) set(self.get_worker_grades())
): ):
@@ -1509,17 +1540,20 @@ class RotaBuilder(object):
) )
) )
self.model.constraints.add( try:
self.constraint_options["max_shifts_per_week"] self.model.constraints.add(
>= sum( self.constraint_options["max_shifts_per_week"]
self.model.works[worker.id, w, day, shiftname] >= sum(
# for shiftname in self.get_shift_names_by_week_day(week, day) self.model.works[worker.id, w, day, shiftname]
# for day in self.days # for shiftname in self.get_shift_names_by_week_day(week, day)
for w, day, shiftname in self.get_all_shiftname_combinations( # for day in self.days
week=week for w, day, shiftname in self.get_all_shiftname_combinations(
week=week
)
) )
) )
) except ValueError:
pass
# TODO: consider excluding shifts that span relevant period # TODO: consider excluding shifts that span relevant period
if ( if (
@@ -2350,6 +2384,9 @@ class RotaBuilder(object):
""" """
self.shifts.extend(shifts) self.shifts.extend(shifts)
def clear_shifts(self) -> None:
self.shifts = []
def get_shift_names_by_week_day(self, week, day: DayStr) -> set: def get_shift_names_by_week_day(self, week, day: DayStr) -> set:
"""Returns the shifts required for a specific day """Returns the shifts required for a specific day
@@ -2701,8 +2738,14 @@ class RotaBuilder(object):
return self.start_date + datetime.timedelta(weeks=week) return self.start_date + datetime.timedelta(weeks=week)
# RESULTS # RESULTS
def export_rota_to_html(self, filename: str = "rota"): def export_rota_to_html(self, filename: str = "rota", folder = None):
output_file = Path("output", f"{filename}.html") if folder is not None:
output_file = Path("output", folder, f"{filename}.html")
else:
output_file = Path("output", f"{filename}.html")
# Make sure path exits
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, "w") as f: with open(output_file, "w") as f:
f.write(self.get_worker_timetable_html(True)) f.write(self.get_worker_timetable_html(True))
+27 -13
View File
@@ -4,6 +4,7 @@ from typing import Iterable, List, Literal, Optional
from pydantic import BaseModel, Extra, validator from pydantic import BaseModel, Extra, validator
from rich.pretty import pprint from rich.pretty import pprint
from rota.console import console
# from .shifts import RotaBuilder, days, sites # from .shifts import RotaBuilder, days, sites
import uuid import uuid
@@ -199,27 +200,37 @@ class Worker(BaseModel):
(self.id, week, day) (self.id, week, day)
] = f"OOP ({oop_name})".format(self.oop) ] = f"OOP ({oop_name})".format(self.oop)
days_to_end = (self.calculated_end_date - Rota.start_date).days
# loop throught dates converting to week / day combination # loop throught dates converting to week / day combination
for item in self.pref_not_to_work: for item in self.pref_not_to_work:
days_from_start = (item.date - Rota.start_date).days days_from_start = (item.date - Rota.start_date).days
week = days_from_start // 7 + 1 # Ignore dates past the end of the rota (or end date)
day = Rota.days[(days_from_start % 7)] if days_from_start < days_to_end:
# Weight the value to take into account the number of preferences week = days_from_start // 7 + 1
# 1 is added to the total number of requests to ensure they do not outweight day = Rota.days[(days_from_start % 7)]
# a single (or fewer) request(s) # Weight the value to take into account the number of preferences
Rota.pref_not_to_work[(self.id, week, day)] = 1 / ( # 1 is added to the total number of requests to ensure they do not outweight
len(self.pref_not_to_work) + 1 # a single (or fewer) request(s)
) Rota.pref_not_to_work[(self.id, week, day)] = 1 / (
Rota.pref_not_to_work_reason[(self.id, week, day)] = item.reason len(self.pref_not_to_work) + 1
)
Rota.pref_not_to_work_reason[(self.id, week, day)] = item.reason
# print(not_available_to_work) # print(not_available_to_work)
# loop throught dates converting to week / day combination # loop throught dates converting to week / day combination
unavailable_set = set()
for item in self.not_available_to_work: for item in self.not_available_to_work:
days_from_start = (item.date - Rota.start_date).days days_from_start = (item.date - Rota.start_date).days
week = days_from_start // 7 + 1 # Ignore dates past the end of the rota (or end date)
day = Rota.days[(days_from_start % 7)] if days_from_start < days_to_end:
Rota.unavailable_to_work.add((self.id, week, day)) week = days_from_start // 7 + 1
Rota.unavailable_to_work_reason[(self.id, week, day)] = item.reason day = Rota.days[(days_from_start % 7)]
Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[(self.id, week, day)] = item.reason
unavailable_set.add((week, day))
for item in self.work_requests: for item in self.work_requests:
days_from_start = (item.date - Rota.start_date).days days_from_start = (item.date - Rota.start_date).days
@@ -257,6 +268,9 @@ class Worker(BaseModel):
if days_to_work < 1: if days_to_work < 1:
self.fte_adj = 0 self.fte_adj = 0
if days_to_work > 0 and (days_to_work - len(unavailable_set)) / days_to_work < 0.3:
console.print(f"Warning {self.name} has excessive unavailability [{len(unavailable_set)}/{days_to_work}, adjusted fte={self.fte_adj}] (this may be infeasible)", style="red")
def __lt__(self, other) -> bool: def __lt__(self, other) -> bool:
return (self.site, self.grade, self.fte_adj, self.name) < ( return (self.site, self.grade, self.fte_adj, self.name) < (
other.site, other.site,
@@ -85,7 +85,7 @@ class TestWorkerRequests:
force_as_block=False, force_as_block=False,
), ),
) )
with pytest.raises(ValueError): with pytest.raises(InvalidShift):
Rota.add_grade_constraint_by_week([1], [1,2,3], ["b"]) Rota.add_grade_constraint_by_week([1], [1,2,3], ["b"])
Rota.build_and_solve(options={"ratio": 0.000}) Rota.build_and_solve(options={"ratio": 0.000})
+106 -24
View File
@@ -4,6 +4,7 @@ from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, days
from rota.workers import Worker from rota.workers import Worker
def generate_basic_rota(weeks_to_rota=10): def generate_basic_rota(weeks_to_rota=10):
start_date = datetime.date(2022, 3, 7) start_date = datetime.date(2022, 3, 7)
@@ -38,18 +39,23 @@ class TestNightShifts:
def test_nights(self): def test_nights(self):
Rota = generate_basic_rota() Rota = generate_basic_rota()
Rota.add_shifts( Rota.add_shifts(
SingleShift( SingleShift(
sites=("group1", "group2"), name="n", length= 12.5, days=days[5:], sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True, force_as_block=True,
constraint=[{"name": "night"}, {"name": "pre", "options": 2}], constraint=[{"name": "night"}, {"name": "pre", "options": 2}],
workers_required=2, workers_required=2,
), ),
SingleShift( SingleShift(
sites=("group1", "group2"), name="b", length= 12.5, days=days[:3], sites=("group1", "group2"),
name="b",
length=12.5,
days=days[:3],
force_as_block=False, force_as_block=False,
#constraint=[{"name": "night"}], # constraint=[{"name": "night"}],
), ),
) )
@@ -62,18 +68,23 @@ class TestNightShifts:
def test_nights_pre3(self): def test_nights_pre3(self):
Rota = generate_basic_rota() Rota = generate_basic_rota()
Rota.add_shifts( Rota.add_shifts(
SingleShift( SingleShift(
sites=("group1", "group2"), name="n", length= 12.5, days=days[5:], sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True, force_as_block=True,
constraint=[{"name": "night"}, {"name": "pre", "options": 3}], constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
workers_required=2, workers_required=2,
), ),
SingleShift( SingleShift(
sites=("group1", "group2"), name="b", length= 12.5, days=days[:2], sites=("group1", "group2"),
name="b",
length=12.5,
days=days[:2],
force_as_block=False, force_as_block=False,
#constraint=[{"name": "night"}], # constraint=[{"name": "night"}],
), ),
) )
@@ -85,18 +96,23 @@ class TestNightShifts:
def test_nights_pre3_2(self): def test_nights_pre3_2(self):
Rota = generate_basic_rota() Rota = generate_basic_rota()
Rota.add_shifts( Rota.add_shifts(
SingleShift( SingleShift(
sites=("group1", "group2"), name="n", length= 12.5, days=days[5:], sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True, force_as_block=True,
constraint=[{"name": "night"}, {"name": "pre", "options": 3}], constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
workers_required=1, workers_required=1,
), ),
SingleShift( SingleShift(
sites=("group1", "group2"), name="b", length= 12.5, days=days[:3], sites=("group1", "group2"),
name="b",
length=12.5,
days=days[:3],
force_as_block=False, force_as_block=False,
#constraint=[{"name": "night"}], # constraint=[{"name": "night"}],
), ),
) )
@@ -108,18 +124,23 @@ class TestNightShifts:
def test_nights_fail(self): def test_nights_fail(self):
Rota = generate_basic_rota() Rota = generate_basic_rota()
Rota.add_shifts( Rota.add_shifts(
SingleShift( SingleShift(
sites=("group1", "group2"), name="n", length= 12.5, days=days[5:], sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True, force_as_block=True,
constraint=[{"name": "night"}, {"name": "pre", "options": 3}], constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
workers_required=2, workers_required=2,
), ),
SingleShift( SingleShift(
sites=("group1", "group2"), name="b", length= 12.5, days=days[:3], sites=("group1", "group2"),
name="b",
length=12.5,
days=days[:3],
force_as_block=False, force_as_block=False,
#constraint=[{"name": "night"}], # constraint=[{"name": "night"}],
), ),
) )
@@ -132,29 +153,90 @@ class TestNightShifts:
Rota.constraint_options["max_night_frequency"] = 0 Rota.constraint_options["max_night_frequency"] = 0
Rota.add_shifts( Rota.add_shifts(
SingleShift( SingleShift(
sites=("group1", "group2"), name="n", length= 12.5, days=days[5:], sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True, force_as_block=True,
constraint=[{"name": "night"}], constraint=[{"name": "night"}],
workers_required=2, workers_required=2,
), ),
SingleShift( SingleShift(
sites=("group1", "group2"), name="m", length= 12.5, days=days[:5], sites=("group1", "group2"),
name="m",
length=12.5,
days=days[:5],
force_as_block=True, force_as_block=True,
#constraint=[{"name": "night"}, {"name": "pre", "options": 3}], # constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
#constraint=[{"name": "night"}], # constraint=[{"name": "night"}],
workers_required=2, workers_required=2,
), ),
#SingleShift( # SingleShift(
# sites=("group1", "group2"), name="b", length= 12.5, days=days[:3], # sites=("group1", "group2"), name="b", length= 12.5, days=days[:3],
# force_as_block=False, # force_as_block=False,
# #constraint=[{"name": "night"}], # #constraint=[{"name": "night"}],
#), # ),
) )
Rota.build_and_solve(options={"ratio": 0.000}) Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night") Rota.export_rota_to_html("night")
assert Rota.results.solver.status == "ok" assert Rota.results.solver.status == "ok"
def test_nights_pre_wrap_around(self):
Rota = generate_basic_rota(weeks_to_rota=8)
Rota.constraint_options["max_night_frequency"] = 0
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[:2],
force_as_block=True,
constraint=[{"name": "night"},
{"name": "pre", "options": 5},{"name": "post", "options": 5},
],
workers_required=3,
),
# SingleShift(
# sites=("group1", "group2"), name="m", length= 12.5, days=days[:5],
# force_as_block=True,
# #constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
# #constraint=[{"name": "night"}],
# workers_required=2,
# ),
# SingleShift(
# sites=("group1", "group2"), name="b", length= 12.5, days=days[:3],
# force_as_block=False,
# #constraint=[{"name": "night"}],
# ),
)
Rota.add_workers(
[
Worker(
name="worker3",
site="group1",
grade=1,
not_available_to_work=[
{
"date": Rota.rota_end_date - datetime.timedelta(days=1),
"reason": "no wanna",
},
#{
# "date": Rota.rota_end_date - datetime.timedelta(days=5),
# "reason": "no wanna",
#},
],
),
]
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
assert Rota.results.solver.status == "ok"