major test refactoring

This commit is contained in:
Ross
2025-06-05 23:40:28 +01:00
parent b9ca76aea1
commit a647416720
13 changed files with 1690 additions and 2282 deletions
+1
View File
@@ -0,0 +1 @@
/home/ross/proc_rota/output/timetable.css
+1
View File
@@ -0,0 +1 @@
/home/ross/proc_rota/output/timetable.js
+5 -4
View File
@@ -392,6 +392,7 @@ class RotaBuilder(object):
debug_if_fail: bool = False, debug_if_fail: bool = False,
): ):
console.print("Setting up solver") console.print("Setting up solver")
console.print(f"{options=}")
if solver == "scip": if solver == "scip":
if "seconds" in options: if "seconds" in options:
@@ -516,6 +517,9 @@ class RotaBuilder(object):
self.build_workers() self.build_workers()
self.build_model() self.build_model()
console.print("Building model")
console.print(f"{options=}")
if solve: if solve:
self.solve_model( self.solve_model(
options=options, debug_if_fail=debug_if_fail, solver=solver options=options, debug_if_fail=debug_if_fail, solver=solver
@@ -2203,7 +2207,6 @@ class RotaBuilder(object):
self.get_week_start_date(week) self.get_week_start_date(week)
): ):
# Force shifts to be assigned in blocks # Force shifts to be assigned in blocks
try:
self.model.constraints.add( self.model.constraints.add(
self.model.shift_week_worker_assigned[ self.model.shift_week_worker_assigned[
shift.name, week, worker.id shift.name, week, worker.id
@@ -2214,8 +2217,6 @@ class RotaBuilder(object):
for day in shift.days for day in shift.days
) )
) )
except KeyError:
pass # blocks with workerd requirements
# #
@@ -3213,7 +3214,7 @@ class RotaBuilder(object):
shifts = [] shifts = []
for shift in self.shifts: for shift in self.shifts:
if shift.start_date < date <= shift.end_date: if shift.start_date <= date < shift.end_date:
if shift.get_worker_requirement_by_date(date): if shift.get_worker_requirement_by_date(date):
shifts.append(shift) shifts.append(shift)
+139
View File
@@ -0,0 +1,139 @@
import pytest
import datetime
from rota.shifts import RotaBuilder, SingleShift, days
from rota.workers import Worker
@pytest.fixture
def demo_rota_balance_sites():
weeks_to_rota = 8
start_date = datetime.date(2022, 3, 7)
Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(name="worker2", site="group1", grade=1)
worker3 = Worker(name="worker3", site="group2", grade=1)
worker4 = Worker(name="worker4", site="group2", grade=1)
worker5 = Worker(name="worker5", site="group3", grade=1)
worker6 = Worker(name="worker6", site="group3", grade=1)
Rota.add_workers((worker1, worker2, worker3, worker4, worker5, worker6))
return Rota
def test_balance_blocks_across_2_groups(demo_rota_balance_sites):
Rota = demo_rota_balance_sites
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="weekday_night",
length=12.5,
days=days[:4],
balance_offset=40,
workers_required=2,
force_as_block=True,
constraint=[{"name": "balance_across_groups"}],
),
)
Rota.constraint_options["balance_nights_across_sites"] = False
Rota.constraint_options["balance_nights"] = False
Rota.constraint_options["constrain_time_off_after_nights"] = False
# Worker 5 and 6 don't have any valid shifts so this should fail
#with pytest.raises(WarningTermination):
# Rota.build_and_solve(options={"ratio": 0.0})
# Unless we remove the warning
Rota.terminate_on_warning.remove("Worker/no valid shifts")
Rota.build_and_solve(options={"ratio": 0.0})
assert len(Rota.get_warnings("Worker/no valid shifts")) == 2
Rota.export_rota_to_html("test5")
group_workers = Rota.get_workers_by_group()
for group in group_workers:
shift_patterns = []
for w in group_workers[group]:
shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") <= 1
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
def test_balance_blocks_across_2_groups_unbalanced(demo_rota_balance_sites):
Rota = demo_rota_balance_sites
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="weekday_night",
length=12.5,
days=days[:4],
balance_offset=40,
workers_required=3,
force_as_block=True,
constraint=[{"name": "balance_across_groups"}],
),
)
Rota.constraint_options["balance_nights_across_sites"] = False
Rota.constraint_options["balance_nights"] = False
Rota.constraint_options["constrain_time_off_after_nights"] = False
try:
Rota.terminate_on_warning.remove("Worker/no valid shifts")
except ValueError:
# Seems to happen with sequential test running
pass
Rota.build_and_solve(options={"ratio": 0.1})
group_workers = Rota.get_workers_by_group()
for group in group_workers:
shift_patterns = []
for w in group_workers[group]:
shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") <= 2
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
def test_balance_blocks_across_3_groups(demo_rota_balance_sites):
Rota = demo_rota_balance_sites
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="weekday_night",
length=12.5,
days=days[:4],
balance_offset=40,
workers_required=3,
force_as_block=True,
constraint=[{"name": "balance_across_groups"}],
),
)
Rota.constraint_options["balance_nights_across_sites"] = False
Rota.constraint_options["balance_nights"] = False
Rota.constraint_options["constrain_time_off_after_nights"] = False
Rota.build_and_solve(options={"ratio": 0.1})
group_workers = Rota.get_workers_by_group()
for group in group_workers:
shift_patterns = []
for w in group_workers[group]:
shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") <= 1
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
+201 -258
View File
@@ -1,284 +1,227 @@
import datetime import datetime
import pytest import pytest
from pytest import approx from pytest import approx
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days from rota.shifts import 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)
Rota = RotaBuilder( Rota = RotaBuilder(
start_date, start_date,
weeks_to_rota=weeks_to_rota, weeks_to_rota=weeks_to_rota,
) )
Rota.constraint_options["balance_weekends"] = True Rota.constraint_options["balance_weekends"] = True
# Add a few workers
Rota.add_workers( Rota.add_workers(
[ [
Worker(name="worker1", site="group1", grade=1), Worker(name="worker1", site="group1", grade=1),
Worker(name="worker2", site="group1", grade=1), Worker(name="worker2", site="group1", grade=1),
Worker(name="worker3", site="group2", grade=1), Worker(name="worker3", site="group2", grade=1),
# Worker(name="worker4", site="group2", grade=1),
# Worker(name="worker5", site="group2", grade=1),
# Worker(name="worker6", site="group2", grade=1, fte=50),
] ]
) )
return Rota return Rota
def test_basic_balancing():
Rota = generate_basic_rota()
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days,
balance_offset=2,
workers_required=1,
force_as_block=False,
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days,
balance_offset=2,
workers_required=1,
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.001})
Rota.export_rota_to_html("basic_balancing")
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
assert worker_shifts["a"] in (approx(22), approx(23), approx(24), approx(25))
assert worker_shifts["b"] in (approx(22), approx(23), approx(24), approx(25))
class TestBalancing: def test_weighted_shift_balancing():
def test_basic_balancing(self): Rota = generate_basic_rota(20)
Rota = generate_basic_rota() Rota.add_shifts(
SingleShift(
sites=["group1", "group2"],
name="a",
length=12.5,
days=days,
balance_weighting=10,
workers_required=1,
force_as_block=False,
constraint=[{"name": "pre","options": "2"}, {"name": "post","options": "2"}],
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days,
workers_required=1,
force_as_block=False,
),
SingleShift(
sites=("group1", "group2"),
name="c",
length=12.5,
days=days[0],
workers_required=1,
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.0001})
Rota.export_rota_to_html("basic_balancing_weighted_shifts")
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
assert worker_shifts["a"] in (approx(46), approx(47), approx(48))
Rota.add_shifts( def test_weighted_shift_balancing2():
SingleShift( Rota = generate_basic_rota(23)
sites=("group1", "group2"), Rota.add_shifts(
name="a", SingleShift(
length=12.5, sites=("group1", "group2"),
days=days, name="a",
balance_offset=2, length=12.5,
workers_required=1, days=days,
force_as_block=False, workers_required=1,
), force_as_block=False,
SingleShift( ),
sites=("group1", "group2"), SingleShift(
name="b", sites=("group1", "group2"),
length=12.5, name="b",
days=days, length=12.5,
balance_offset=2, days=days,
workers_required=1, balance_weighting=8,
force_as_block=False, workers_required=1,
), force_as_block=False,
) ),
SingleShift(
sites=("group1", "group2"),
name="c",
length=12.5,
days=days[0],
workers_required=1,
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.0001})
Rota.export_rota_to_html("basic_balancing_weighted_shifts2")
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
assert worker_shifts["b"] in (approx(53), approx(54), approx(55))
Rota.build_and_solve(options={"ratio": 0.001}) def test_weighted_shift_balancing3():
Rota.export_rota_to_html("basic_balancing") Rota = generate_basic_rota(23)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days,
workers_required=1,
force_as_block=False,
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days,
balance_weighting=1,
workers_required=1,
force_as_block=False,
),
SingleShift(
sites=("group1", "group2"),
name="c",
length=12.5,
days=days[0],
balance_weighting=8,
workers_required=1,
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.0001})
Rota.export_rota_to_html("basic_balancing_weighted_shifts3")
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
assert worker_shifts["c"] in (approx(7), approx(8))
shift_summary = Rota.get_shift_summary_dict() def test_weighted_shift_balancing4():
for worker_name in shift_summary: Rota = generate_basic_rota(10)
worker_shifts = shift_summary[worker_name] Rota.add_worker(
Worker(name="worker4", site="group3", grade=1),
)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days,
balance_offset=99,
balance_weighting=8,
workers_required=2,
force_as_block=False,
),
SingleShift(
sites=("group2", "group3"),
name="b",
length=12.5,
days=days,
balance_offset=99,
workers_required=1,
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("basic_balancing_weighted_shifts4")
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
assert worker_shifts["a"] in (0, 46, 47, 48)
#worker = Rota.get_worker_by_name(worker_name) def test_weighted_shift_balancing5():
assert worker_shifts["a"] in (approx(22), approx(23), approx(24), approx(25)) Rota = generate_basic_rota(10)
assert worker_shifts["b"] in (approx(22), approx(23), approx(24), approx(25)) Rota.add_worker(
Worker(name="worker4", site="group3", grade=1),
def test_weighted_shift_balancing(self): )
Rota = generate_basic_rota(20) Rota.add_shifts(
SingleShift(
Rota.add_shifts( sites=("group1", "group2"),
SingleShift( name="a",
sites=["group1", "group2"], length=12.5,
name="a", days=days,
length=12.5, balance_offset=99,
days=days, workers_required=2,
balance_weighting=10, force_as_block=False,
workers_required=1, ),
force_as_block=False, SingleShift(
constraint=[{"name": "pre","options": "2"}, {"name": "post","options": "2"}], sites=("group2", "group3"),
), name="b",
SingleShift( length=12.5,
sites=("group1", "group2"), days=days,
name="b", balance_offset=99,
length=12.5, balance_weighting=4,
days=days, workers_required=1,
workers_required=1, force_as_block=False,
force_as_block=False, ),
), )
SingleShift( Rota.build_and_solve(options={"ratio": 0.000})
sites=("group1", "group2"), Rota.export_rota_to_html("basic_balancing_weighted_shifts5")
name="c", shift_summary = Rota.get_shift_summary_dict()
length=12.5, for worker_name in shift_summary:
days=days[0], worker_shifts = shift_summary[worker_name]
workers_required=1, assert worker_shifts["b"] in (0, 35)
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.0001})
Rota.export_rota_to_html("basic_balancing_weighted_shifts")
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
assert worker_shifts["a"] in (approx(46), approx(47), approx(48))
#assert worker_shifts["b"] in (46, 47, 48)
# assert worker_shifts["b"] in (22, 23, 24, 25)
def test_weighted_shift_balancing2(self):
Rota = generate_basic_rota(23)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days,
# balance_weighting=4,
workers_required=1,
force_as_block=False,
# constraint=["preclear2", "postclear2"],
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days,
balance_weighting=8,
workers_required=1,
force_as_block=False,
),
SingleShift(
sites=("group1", "group2"),
name="c",
length=12.5,
days=days[0],
workers_required=1,
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.0001})
Rota.export_rota_to_html("basic_balancing_weighted_shifts2")
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
# assert worker_shifts["a"] in (46,47,48)
# assert worker_shifts["c"] in (46,47,48)
assert worker_shifts["b"] in (approx(53), approx(54), approx(55))
def test_weighted_shift_balancing3(self):
Rota = generate_basic_rota(23)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days,
# balance_weighting=4,
workers_required=1,
force_as_block=False,
# constraint=["preclear2", "postclear2"],
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days,
balance_weighting=1,
workers_required=1,
force_as_block=False,
),
SingleShift(
sites=("group1", "group2"),
name="c",
length=12.5,
days=days[0],
balance_weighting=8,
workers_required=1,
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.0001})
Rota.export_rota_to_html("basic_balancing_weighted_shifts3")
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
# assert worker_shifts["a"] in (46,47,48)
assert worker_shifts["c"] in (approx(7), approx(8))
#assert worker_shifts["b"] in (53, 54)
def test_weighted_shift_balancing4(self):
Rota = generate_basic_rota(10)
Rota.add_worker(
Worker(name="worker4", site="group3", grade=1),
)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days,
balance_offset=99,
balance_weighting=8,
workers_required=2,
force_as_block=False,
# constraint=["preclear2", "postclear2"],
),
SingleShift(
sites=("group2", "group3"),
name="b",
length=12.5,
days=days,
balance_offset=99,
# balance_weighting=4,
workers_required=1,
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("basic_balancing_weighted_shifts4")
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
assert worker_shifts["a"] in (0, 46, 47, 48)
def test_weighted_shift_balancing5(self):
Rota = generate_basic_rota(10)
Rota.add_worker(
Worker(name="worker4", site="group3", grade=1),
)
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days,
balance_offset=99,
# balance_weighting=8,
workers_required=2,
force_as_block=False,
# constraint=["preclear2", "postclear2"],
),
SingleShift(
sites=("group2", "group3"),
name="b",
length=12.5,
days=days,
balance_offset=99,
balance_weighting=4,
workers_required=1,
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("basic_balancing_weighted_shifts5")
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
assert worker_shifts["b"] in (0, 35)
+325
View File
@@ -0,0 +1,325 @@
import pytest
import datetime
from rota.shifts import RotaBuilder, SingleShift, days
from rota.workers import Worker
@pytest.fixture
def limit_constraint_rota():
weeks_to_rota = 8
start_date = datetime.date(2022, 3, 7)
Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
worker1 = Worker(name="worker1", site="group1", grade=2, remote_site="group1")
worker2 = Worker(name="worker2", site="group1", grade=2, remote_site="group1")
worker3 = Worker(name="worker3", site="group1", grade=2, remote_site="group1")
worker4 = Worker(name="worker4", site="group3", grade=3, remote_site="group2")
worker5 = Worker(name="worker5", site="group3", grade=3, remote_site="group2")
worker6 = Worker(name="worker6", site="group3", grade=3, remote_site="group2")
Rota.add_workers((worker1, worker2, worker3, worker4, worker5, worker6))
Rota.shifts = []
return Rota
def test_constraint_limit_grades(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="weekday_night",
length=12.5,
days=days,
balance_offset=60,
workers_required=4,
force_as_block=True,
constraint=[{"name": "limit_grade_number", "options": {2: 1}}],
),
)
Rota.build_and_solve(options={"ratio": 0.01})
Rota.export_rota_to_html("constraint_limit_grades", folder="tests")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
grade_workers = Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = [Rota.get_worker_shift_list(w) for w in grade_workers[grade]]
zipped_lists = list(zip(*shift_patterns))
limit = 1 if grade == 2 else 3
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
def test_constraint_limit_grades2(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="weekday_night",
length=12.5,
days=days,
balance_offset=40,
workers_required=4,
force_as_block=True,
constraint=[{"name": "limit_grade_number", "options": {3: 2}}],
),
)
Rota.constraint_options["balance_shifts_quadratic"] = True
Rota.build_and_solve(options={"ratio": 0.1})
Rota.export_rota_to_html("test9")
grade_workers = Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = [Rota.get_worker_shift_list(w) for w in grade_workers[grade]]
zipped_lists = list(zip(*shift_patterns))
limit = 2 if grade == 3 else 2
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
def test_constraint_limit_grades3(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.workers = []
worker1 = Worker(name="worker1", site="group1", grade=2, remote_site="group1")
worker2 = Worker(name="worker2", site="group1", grade=2, remote_site="group1")
worker3 = Worker(name="worker3", site="group1", grade=2, remote_site="group1")
worker4 = Worker(name="worker4", site="group3", grade=3, remote_site="group2")
worker5 = Worker(name="worker5", site="group3", grade=3, remote_site="group2")
worker6 = Worker(name="worker6", site="group3", grade=3, remote_site="group2")
worker7 = Worker(name="worker7", site="group3", grade=2, remote_site="group2")
Rota.add_workers((worker1, worker2, worker3, worker4, worker5, worker6, worker7))
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="weekday_night",
length=12.5,
days=days,
balance_offset=60,
workers_required=4,
force_as_block=True,
constraint=[{"name": "limit_grade_number", "options": {2: 3, 3: 1}}],
),
)
Rota.constraint_options["balance_shifts_quadratic"] = True
Rota.build_and_solve(options={"ratio": 0.01})
Rota.export_rota_to_html("test9")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
grade_workers = Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = [Rota.get_worker_shift_list(w) for w in grade_workers[grade]]
zipped_lists = list(zip(*shift_patterns))
limit = 3 if grade == 2 else 1
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
def test_constraint_limit_grades4(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="weekday_night",
length=12.5,
days=days,
balance_offset=40,
workers_required=4,
force_as_block=True,
constraint=[{"name": "limit_grade_number", "options": {2: 4, 3: 0}}],
),
)
Rota.constraint_options["balance_shifts_quadratic"] = True
Rota.build_and_solve(options={"ratio": 0.1})
assert Rota.results.solver.termination_condition == "infeasible"
def test_constraint_minimum_grades(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="weekday_night",
length=12.5,
days=days,
balance_offset=40,
workers_required=1,
force_as_block=True,
constraint=[{"name": "minimum_grade_number", "options": (3, 1)}],
),
)
Rota.constraint_options["balance_shifts_quadratic"] = True
Rota.build_and_solve(options={"ratio": 0.1})
Rota.export_rota_to_html("test9")
assert Rota.results.solver.status == "ok"
grade_workers = Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = [Rota.get_worker_shift_list(w) for w in grade_workers[grade]]
zipped_lists = list(zip(*shift_patterns))
limit = 1 if grade > 2 else 0
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
def test_constraint_minimum_grades2(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="weekday_night",
length=12.5,
days=days,
balance_offset=40,
workers_required=4,
force_as_block=True,
constraint=[{"name": "minimum_grade_number", "options": (3, 3)}],
),
)
Rota.constraint_options["balance_shifts_quadratic"] = True
Rota.build_and_solve(options={"ratio": 0.1})
Rota.export_rota_to_html("test9")
grade_workers = Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = [Rota.get_worker_shift_list(w) for w in grade_workers[grade]]
zipped_lists = list(zip(*shift_patterns))
limit = 3 if grade > 2 else 1
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
assert Rota.results.solver.status == "ok"
def test_constraint_minimum_grades3(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="weekday_night",
length=12.5,
days=days,
balance_offset=40,
workers_required=4,
force_as_block=True,
constraint=[{"name": "minimum_grade_number", "options": (3, 0)}],
),
)
Rota.constraint_options["balance_shifts_quadratic"] = True
Rota.build_and_solve(options={"ratio": 0.1})
Rota.export_rota_to_html("test9")
assert Rota.results.solver.status == "ok"
def test_constraint_minimum_grades_no_valid_worker(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="weekday_night",
length=12.5,
days=days,
balance_offset=40,
workers_required=4,
force_as_block=True,
constraint=[{"name": "minimum_grade_number", "options": (4, 1)}],
),
)
Rota.constraint_options["balance_shifts_quadratic"] = True
with pytest.raises(ValueError):
Rota.build_and_solve(options={"ratio": 0.1})
def test_constraint_require_remote_site_presence_week(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="night",
length=12.5,
days=days,
balance_offset=40,
workers_required=2,
force_as_block=True,
constraint=[
{
"name": "require_remote_site_presence_week",
"options": ("group1", 2),
}
],
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("remote1")
group_workers = Rota.get_workers_by_group()
for group in group_workers:
shift_patterns = [Rota.get_worker_shift_list(w) for w in group_workers[group]]
zipped_lists = list(zip(*shift_patterns))
limit = 2 if group == "group1" else 0
for day_shifts in zipped_lists:
assert day_shifts.count("night") == limit
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
def test_constraint_require_remote_site_presence_week2(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.constraint_options["minimise_shift_diffs"] = False
Rota.constraint_options["balance_shifts_quadratic"] = False
Rota.constraint_options["balance_shift"] = False
Rota.constraint_options["balance_nights_across_sites"] = True
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="night",
length=12.5,
days=days,
balance_offset=20,
workers_required=3,
force_as_block=True,
constraint=[
{
"name": "require_remote_site_presence_week",
"options": ("group1", 2),
}
],
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("remote2")
group_workers = Rota.get_workers_by_remote_group()
for group in group_workers:
shift_patterns = [Rota.get_worker_shift_list(w) for w in group_workers[group]]
zipped_lists = list(zip(*shift_patterns))
if group == "group1":
for day_shifts in zipped_lists:
assert day_shifts.count("night") >= 2
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
def test_constraint_require_remote_site_presence_week3(limit_constraint_rota):
Rota = limit_constraint_rota
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1", "group2", "group3"),
name="night",
length=12.5,
days=days,
balance_offset=20,
workers_required=3,
force_as_block=True,
constraint=[
{
"name": "require_remote_site_presence_week",
"options": ("group2", 1),
}
],
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("remote3")
group_workers = Rota.get_workers_by_remote_group()
for group in group_workers:
shift_patterns = [Rota.get_worker_shift_list(w) for w in group_workers[group]]
zipped_lists = list(zip(*shift_patterns))
if group == "group2":
for day_shifts in zipped_lists:
assert day_shifts.count("night") >= 1
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
+146
View File
@@ -0,0 +1,146 @@
import pytest
import datetime
from rota.shifts import RotaBuilder, SingleShift, days
from rota.workers import Worker
@pytest.fixture
def demo_rota_night_unavailable():
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
worker1 = Worker(
name="worker1",
site="group1",
grade=1,
not_available_to_work=[
{
"date": datetime.datetime.strptime("15/03/22", "%d/%m/%y").date(),
"reason": "****",
},
],
)
worker2 = Worker(
name="worker2",
site="group1",
grade=1,
not_available_to_work=[
{
"date": datetime.datetime.strptime("14/03/22", "%d/%m/%y").date(),
"reason": "****",
}
],
)
Rota.add_workers((worker1, worker2))
return Rota
def test_basic_assignment(demo_rota_night_unavailable):
Rota = demo_rota_night_unavailable
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1",),
name="night_weekend",
length=12.5,
days=days[5:],
constraint=[{"name": "night"}],
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("testnight")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
summary = Rota.get_shift_summary_dict()
for worker in summary:
assert summary[worker]["night_weekend"] == 10
def test_assign_night_prior_to_unavailablity(demo_rota_night_unavailable):
Rota = demo_rota_night_unavailable
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1",),
name="night_weekend",
length=12.5,
days=days[5:],
workers_required=2,
constraint=[{"name": "night"}],
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("testnight_unavail")
assert Rota.results.solver.status in ("warning", "error")
assert Rota.results.solver.termination_condition == "infeasible"
def test_assign_non_night_prior_to_unavailablity(demo_rota_night_unavailable):
Rota = demo_rota_night_unavailable
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1",),
name="night_weekend",
length=12.5,
days=days[5:],
workers_required=2,
#constraint=[{"name": "night"}],
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("testnight_unavail")
assert Rota.results.solver.status == "ok"
def test_assign_prior_to_unavailablity_non_night(demo_rota_night_unavailable):
Rota = demo_rota_night_unavailable
Rota.shifts = []
Rota.add_shifts(
SingleShift(
sites=("group1",),
name="night_weekend",
length=12.5,
days=days[5:],
workers_required=2,
constraint=[],
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("testnight")
assert Rota.results.solver.status == "ok"
def test_assign_split(demo_rota_night_unavailable):
Rota = demo_rota_night_unavailable
Rota.shifts = []
worker3 = Worker(
name="worker3",
site="group1",
grade=1,
not_available_to_work=(
{"date": datetime.datetime.strptime("13/03/22", "%d/%m/%y").date()},
),
)
worker4 = Worker(
name="worker4",
site="group1",
grade=1,
not_available_to_work=(
{"date": datetime.datetime.strptime("12/03/22", "%d/%m/%y").date()},
),
)
Rota.add_workers((worker3, worker4))
Rota.add_shifts(
SingleShift(
sites=("group1",),
name="night_weekend",
length=12.5,
days=days[5:],
workers_required=3,
constraint=[],
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("testnight")
assert Rota.results.solver.status == "ok"
summary = Rota.get_shift_summary_dict()
for worker in summary:
assert summary[worker]["night_weekend"] == 15
+325 -466
View File
@@ -1,512 +1,371 @@
import datetime import datetime
import pytest import pytest
from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, days 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)
Rota = RotaBuilder( Rota = RotaBuilder(
start_date, start_date,
weeks_to_rota=weeks_to_rota, weeks_to_rota=weeks_to_rota,
) )
# Add a few workers
Rota.add_workers( Rota.add_workers(
[ [
Worker(name="worker1", site="group1", grade=1), Worker(name="worker1", site="group1", grade=1),
Worker(name="worker2", site="group1", grade=1), Worker(name="worker2", site="group1", grade=1),
] ]
) )
return Rota return Rota
def date_generator(from_date, days): def date_generator(from_date, days):
n = 0 n = 0
while True: while True:
yield from_date yield from_date
n = n + 1 n = n + 1
if n >= days: if n >= days:
break break
from_date = from_date + datetime.timedelta(days=1) from_date = from_date + datetime.timedelta(days=1)
def test_nights():
Rota = generate_basic_rota()
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}, {"name": "pre", "options": 2}],
workers_required=2,
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days[:3],
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night", folder="tests")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
class TestNightShifts: def test_nights_pre3():
def test_nights(self): Rota = generate_basic_rota()
Rota = generate_basic_rota() Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
workers_required=2,
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days[:2],
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night_pre3", folder="tests")
assert Rota.results.solver.status == "ok"
Rota.add_shifts( def test_nights_pre3_2():
SingleShift( Rota = generate_basic_rota()
sites=("group1", "group2"), Rota.add_shifts(
name="n", SingleShift(
length=12.5, sites=("group1", "group2"),
days=days[5:], name="n",
force_as_block=True, length=12.5,
constraint=[{"name": "night"}, {"name": "pre", "options": 2}], days=days[5:],
workers_required=2, force_as_block=True,
), constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
SingleShift( workers_required=1,
sites=("group1", "group2"), ),
name="b", SingleShift(
length=12.5, sites=("group1", "group2"),
days=days[:3], name="b",
force_as_block=False, length=12.5,
# constraint=[{"name": "night"}], days=days[:3],
), force_as_block=False,
) ),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night_pre3", folder="tests")
assert Rota.results.solver.status == "ok"
Rota.build_and_solve(options={"ratio": 0.000}) def test_nights_fail():
Rota.export_rota_to_html("night") Rota = generate_basic_rota()
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
workers_required=2,
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days[:3],
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
assert Rota.results.solver.status in ("warning", "error")
assert Rota.results.solver.status == "ok" def test_nights2():
assert Rota.results.solver.termination_condition == "optimal" Rota = generate_basic_rota()
Rota.constraint_options["max_night_frequency"] = 0
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=2,
),
SingleShift(
sites=("group1", "group2"),
name="m",
length=12.5,
days=days[:5],
force_as_block=True,
workers_required=2,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night", folder="tests")
assert Rota.results.solver.status == "ok"
def test_nights_pre3(self): def test_nights_pre_wrap_around():
Rota = generate_basic_rota() Rota = generate_basic_rota(weeks_to_rota=8)
Rota.constraint_options["max_night_frequency"] = 0
Rota.add_shifts( Rota.add_shifts(
SingleShift( SingleShift(
sites=("group1", "group2"), sites=("group1", "group2"),
name="n", name="n",
length=12.5, length=12.5,
days=days[5:], days=days[:2],
force_as_block=True, force_as_block=True,
constraint=[{"name": "night"}, {"name": "pre", "options": 3}], constraint=[
workers_required=2, {"name": "night"},
), {"name": "pre", "options": 5},
SingleShift( {"name": "post", "options": 5},
sites=("group1", "group2"), ],
name="b", workers_required=3,
length=12.5, ),
days=days[:2], )
force_as_block=False, Rota.add_workers(
# constraint=[{"name": "night"}], [
), Worker(
) name="worker3",
site="group1",
Rota.build_and_solve(options={"ratio": 0.000}) grade=1,
Rota.export_rota_to_html("night_pre3") not_available_to_work=[
{
assert Rota.results.solver.status == "ok" "date": Rota.rota_end_date - datetime.timedelta(days=1),
"reason": "no wanna",
def test_nights_pre3_2(self): },
Rota = generate_basic_rota()
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
workers_required=1,
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days[:3],
force_as_block=False,
# constraint=[{"name": "night"}],
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night_pre3")
assert Rota.results.solver.status == "ok"
def test_nights_fail(self):
Rota = generate_basic_rota()
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
workers_required=2,
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days[:3],
force_as_block=False,
# constraint=[{"name": "night"}],
),
)
Rota.build_and_solve(options={"ratio": 0.000})
assert Rota.results.solver.status in ("warning", "error")
def test_nights2(self):
Rota = generate_basic_rota()
Rota.constraint_options["max_night_frequency"] = 0
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=2,
),
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.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
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, Rota.build_and_solve(options={"ratio": 0.000})
# #constraint=[{"name": "night"}, {"name": "pre", "options": 3}], Rota.export_rota_to_html("night pre wrap around", folder="tests")
# #constraint=[{"name": "night"}], assert Rota.results.solver.status == "ok"
# 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( def test_nights_max_frequency():
[ Rota = generate_basic_rota()
Worker( Rota.constraint_options["max_night_frequency"] = 1
name="worker3", Rota.add_shifts(
site="group1", SingleShift(
grade=1, sites=("group1", "group2"),
not_available_to_work=[ name="n",
{ length=12.5,
"date": Rota.rota_end_date - datetime.timedelta(days=1), days=days[5:],
"reason": "no wanna", force_as_block=True,
}, constraint=[{"name": "night"}],
# { workers_required=2,
# "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 max frequency", folder="tests")
), assert Rota.results.solver.status == "ok"
]
)
Rota.build_and_solve(options={"ratio": 0.000}) def test_nights_max_frequency_fail():
Rota.export_rota_to_html("night") Rota = generate_basic_rota()
Rota.constraint_options["max_night_frequency"] = 2
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=2,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
assert Rota.results.solver.status in ("warning", "error")
assert Rota.results.solver.status == "ok" def test_nights_max_frequency3():
Rota = generate_basic_rota()
Rota.constraint_options["max_night_frequency"] = 2
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=1,
),
SingleShift(
sites=("group1", "group2"),
name="m",
length=12.5,
days=days[:5],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=1,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night max frequency 3", folder="tests")
assert Rota.results.solver.status in ("warning", "error")
def test_nights_max_frequency(self): def test_nights_max_frequency4():
Rota = generate_basic_rota() Rota = generate_basic_rota()
Rota.constraint_options["max_night_frequency"] = 2
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=1,
),
SingleShift(
sites=("group1", "group2"),
name="m",
length=12.5,
days=days[:5],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=1,
),
)
Rota.add_workers(
[
Worker(name="worker3", site="group1", grade=1),
Worker(name="worker4", site="group1", grade=1),
]
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night max frequency 4", folder="tests")
assert Rota.results.solver.status == "ok"
Rota.constraint_options["max_night_frequency"] = 1 def test_nights_max_frequency_exclusions():
Rota = generate_basic_rota()
Rota.constraint_options["max_night_frequency"] = 2
Rota.constraint_options["max_night_frequency_week_exclusions"] = list(range(10))
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=2,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night max frequency exclusions", folder="tests")
assert Rota.results.solver.status == "ok"
Rota.add_shifts( def test_nights_max_frequency_exclusions2():
SingleShift( Rota = generate_basic_rota(weeks_to_rota=8)
sites=("group1", "group2"), Rota.constraint_options["max_night_frequency"] = 2
name="n", Rota.constraint_options["max_night_frequency_week_exclusions"] = list(range(1,4))
length=12.5, Rota.add_shifts(
days=days[5:], SingleShift(
force_as_block=True, sites=("group1", "group2"),
constraint=[{"name": "night"}], name="n",
workers_required=2, length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=2,
),
)
Rota.add_workers(
[
Worker(
name="worker3",
site="group1",
grade=1,
start_date=Rota.start_date + datetime.timedelta(weeks=4)
), ),
# SingleShift( Worker(
# sites=("group1", "group2"), name="worker4",
# name="m", site="group1",
# length=12.5, grade=1,
# days=days[:5], start_date=Rota.start_date + datetime.timedelta(weeks=4)
# 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.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
assert Rota.results.solver.status == "ok"
def test_nights_max_frequency_fail(self):
Rota = generate_basic_rota()
Rota.constraint_options["max_night_frequency"] = 2
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=2,
), ),
# SingleShift( ]
# sites=("group1", "group2"), )
# name="m", Rota.build_and_solve(options={"ratio": 0.000})
# length=12.5, Rota.export_rota_to_html("night max frequency exclusions 2", folder="tests")
# days=days[:5], assert Rota.results.solver.status == "ok"
# 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.build_and_solve(options={"ratio": 0.000}) def test_nights_max_frequency_exclusions3():
Rota.export_rota_to_html("night") Rota = generate_basic_rota(weeks_to_rota=9)
Rota.constraint_options["max_night_frequency"] = 2
assert Rota.results.solver.status in ("warning", "error") Rota.constraint_options["max_night_frequency_week_exclusions"] = list(range(1,5))
Rota.add_shifts(
def test_nights_max_frequency3(self): SingleShift(
Rota = generate_basic_rota() sites=("group1", "group2"),
name="n",
Rota.constraint_options["max_night_frequency"] = 2 length=12.5,
days=days[5:],
Rota.add_shifts( force_as_block=True,
SingleShift( balance_offset=10,
sites=("group1", "group2"), constraint=[{"name": "night"}],
name="n", workers_required=2,
length=12.5, ),
days=days[5:], )
force_as_block=True, Rota.add_workers(
constraint=[{"name": "night"}], [
workers_required=1, Worker(
name="worker3",
site="group1",
grade=1,
start_date=Rota.start_date + datetime.timedelta(weeks=4)
), ),
SingleShift( Worker(
sites=("group1", "group2"), name="worker4",
name="m", site="group1",
length=12.5, grade=1,
days=days[:5], start_date=Rota.start_date + datetime.timedelta(weeks=4)
force_as_block=True,
# constraint=[{"name": "night"}, {"name": "pre", "options": 3}],
constraint=[{"name": "night"}],
workers_required=1,
), ),
# SingleShift( ]
# sites=("group1", "group2"), name="b", length= 12.5, days=days[:3], )
# force_as_block=False, Rota.build_and_solve(options={"ratio": 0.000})
# #constraint=[{"name": "night"}], Rota.export_rota_to_html("night max frequency exclusions 3", folder="tests")
# ), assert Rota.results.solver.status == "ok"
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
assert Rota.results.solver.status in ("warning", "error")
def test_nights_max_frequency4(self):
Rota = generate_basic_rota()
Rota.constraint_options["max_night_frequency"] = 2
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=1,
),
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=1,
),
# 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,
),
Worker(
name="worker4",
site="group1",
grade=1,
),
]
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
assert Rota.results.solver.status == "ok"
def test_nights_max_frequency_exclusions(self):
Rota = generate_basic_rota()
Rota.constraint_options["max_night_frequency"] = 2
Rota.constraint_options["max_night_frequency_week_exclusions"] = list(range(10))
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=2,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
assert Rota.results.solver.status == "ok"
def test_nights_max_frequency_exclusions2(self):
Rota = generate_basic_rota(weeks_to_rota=8)
Rota.constraint_options["max_night_frequency"] = 2
Rota.constraint_options["max_night_frequency_week_exclusions"] = list(range(1,4))
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=2,
),
)
Rota.add_workers(
[
Worker(
name="worker3",
site="group1",
grade=1,
start_date=Rota.start_date + datetime.timedelta(weeks=4)
),
Worker(
name="worker4",
site="group1",
grade=1,
start_date=Rota.start_date + datetime.timedelta(weeks=4)
),
]
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
assert Rota.results.solver.status == "ok"
def test_nights_max_frequency_exclusions3(self):
# TODO: find out the issues with odd rotas and exclusions
Rota = generate_basic_rota(weeks_to_rota=9)
Rota.constraint_options["max_night_frequency"] = 2
Rota.constraint_options["max_night_frequency_week_exclusions"] = list(range(1,5))
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
balance_offset=10,
constraint=[{"name": "night"}],
workers_required=2,
),
)
Rota.add_workers(
[
Worker(
name="worker3",
site="group1",
grade=1,
start_date=Rota.start_date + datetime.timedelta(weeks=4)
),
Worker(
name="worker4",
site="group1",
grade=1,
start_date=Rota.start_date + datetime.timedelta(weeks=4)
),
]
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
assert Rota.results.solver.status == "ok"
+195 -353
View File
@@ -1,374 +1,216 @@
import pytest import pytest
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
import datetime import datetime
from rota.workers import Worker from rota.workers import Worker
def weeks_from_list(lst): def weeks_from_list(lst):
"""Yield successive n-sized chunks from a lst.""" """Yield successive n-sized chunks from a lst."""
for i in range(0, len(lst), 7): for i in range(0, len(lst), 7):
yield lst[i : i + 7] yield lst[i : i + 7]
def setup_rota(weeks_to_rota=10, start_date=None):
class TestDemoRota: if start_date is None:
def test_nwd(self):
# Set up rota
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7) start_date = datetime.date(2022, 3, 7)
return RotaBuilder(start_date, weeks_to_rota=weeks_to_rota)
self.Rota = RotaBuilder( def test_nwd():
start_date, Rota = setup_rota()
weeks_to_rota=weeks_to_rota, worker1 = Worker(name="worker1", site="group1", grade=1, nwds=[{"day": "Mon"}])
) worker2 = Worker(name="worker2", site="group1", grade=1, nwds=[{"day": "Fri"}])
Rota.add_workers((worker1, worker2))
Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length=12.5, days=days[:5], workers_required=1,
),
SingleShift(
sites=("group1",), name="w", length=12.5, days=days[5:], workers_required=2,
),
)
Rota.build_and_solve(options={"ratio": 0.10})
Rota.export_rota_to_html("nwd1", folder="tests")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
for worker in Rota.workers:
shifts = Rota.get_worker_shift_list(worker)
shifts_string = "".join([i if i != "" else "-" for i in shifts])
if worker.name == "worker1":
assert "wwd" not in shifts_string
if worker.name == "worker2":
assert "dww" not in shifts_string
# Add a few workers def test_nwd_simple_fail():
worker1 = Worker(name="worker1", site="group1", grade=1, nwds=[{"day": "Mon"}]) Rota = setup_rota()
worker2 = Worker(name="worker2", site="group1", grade=1, nwds=[{"day": "Fri"}]) worker1 = Worker(name="worker1", site="group1", grade=1, nwds=[{"day": "Mon"}])
worker2 = Worker(name="worker2", site="group1", grade=1, nwds=[{"day": "Mon"}])
Rota.add_workers((worker1, worker2))
Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length=12.5, days=days[:5], balance_offset=10, workers_required=1,
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("nwd", folder="tests")
assert Rota.results.solver.status in ("warning", "error")
self.Rota.add_workers((worker1, worker2)) def test_nwd_partial_rota():
Rota = setup_rota()
start_date = Rota.start_date
d = start_date + datetime.timedelta(weeks=5)
worker1 = Worker(
name="worker1", site="group1", grade=1,
nwds=[{"day": "Mon", "start_date": start_date, "end_date": d}],
)
worker2 = Worker(
name="worker2", site="group1", grade=1,
nwds=[{"day": "Mon", "start_date": d, "end_date": Rota.rota_end_date}],
)
Rota.add_workers((worker1, worker2))
Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length=12.5, days=days[:5], balance_offset=10, workers_required=1,
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("nwd", folder="tests")
assert Rota.results.solver.status == "ok"
for worker in Rota.workers:
shifts = Rota.get_worker_shift_list(worker)
shifts_string = "".join([i if i != "" else "-" for i in shifts])
if worker.name == "worker1":
for n in range(0, 4):
assert shifts_string[0 + 7 * n] == "-"
if worker.name == "worker2":
for n in range(5, 9):
assert shifts_string[0 + 7 * n] == "-"
# Add a weekday and weekend shift def test_nwd_partial_overlay():
self.Rota.add_shifts( Rota = setup_rota()
SingleShift( start_date = Rota.start_date
sites=("group1",), name="d", length= 12.5, days=days[:5], d = start_date + datetime.timedelta(weeks=5)
# balance_offset=10, d2 = start_date + datetime.timedelta(weeks=6)
workers_required=1, worker1 = Worker(
), name="worker1", site="group1", grade=1,
SingleShift( nwds=[{"day": "Mon", "start_date": start_date, "end_date": d2}],
sites=("group1",), name="w", length= 12.5, days=days[5:], )
workers_required=2, worker2 = Worker(
), name="worker2", site="group1", grade=1,
) nwds=[{"day": "Mon", "start_date": d, "end_date": Rota.rota_end_date}],
)
Rota.add_workers((worker1, worker2))
Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length=12.5, days=days[:5], balance_offset=10, workers_required=1,
),
)
Rota.build_and_solve(options={"ratio": 0.00})
assert Rota.results.solver.status in ("warning", "error")
self.Rota.build_and_solve(options={"ratio": 0.10}) def test_nwd_force_as_block():
Rota = setup_rota()
start_date = Rota.start_date
d = start_date + datetime.timedelta(weeks=4)
worker1 = Worker(
name="worker1", site="group1", grade=1,
nwds=[{"day": "Mon", "start_date": start_date, "end_date": d}],
)
worker2 = Worker(name="worker2", site="group1", grade=1)
worker3 = Worker(name="worker3", site="group1", grade=1)
worker4 = Worker(name="worker4", site="group1", grade=1)
Rota.add_workers((worker1, worker2, worker3, worker4))
Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length=12.5, days=days[:5],
balance_offset=10, workers_required=2,
force_as_block_unless_nwd=True, assign_as_block=True,
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("nwd", folder="tests")
assert Rota.results.solver.status == "ok"
for worker in Rota.workers:
shifts = Rota.get_worker_shift_list(worker)
shifts_string = "".join([i if i != "" else "-" for i in shifts])
for week in weeks_from_list(shifts_string):
assert week in ("-------", "ddddd--")
assert shifts_string.count("d") == 25
self.Rota.export_rota_to_html("nwd1") def test_nwd_force_as_block_force_split():
Rota = setup_rota()
start_date = Rota.start_date
d = start_date + datetime.timedelta(weeks=5)
worker1 = Worker(
name="worker1", site="group1", grade=1,
nwds=[{"day": "Mon", "start_date": start_date, "end_date": d}],
)
worker2 = Worker(name="worker2", site="group1", grade=1)
worker3 = Worker(name="worker3", site="group1", grade=1)
worker4 = Worker(name="worker4", site="group1", grade=1)
Rota.add_workers((worker1, worker2, worker3, worker4))
Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length=12.5, days=days[:5],
workers_required=2, force_as_block_unless_nwd=True, assign_as_block=True,
),
SingleShift(
sites=("group1",), name="w", length=12.5, days=days[5:],
workers_required=4, force_as_block_unless_nwd=True, assign_as_block=True,
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("nwd_block_force_split", folder="tests")
assert self.Rota.results.solver.status == "ok" assert Rota.results.solver.status == "ok"
assert self.Rota.results.solver.termination_condition == "optimal" for worker in Rota.workers:
shifts = Rota.get_worker_shift_list(worker)
for worker in self.Rota.workers: shifts_string = "".join([i if i != "" else "-" for i in shifts])
shifts = self.Rota.get_worker_shift_list(worker) for week in weeks_from_list(shifts_string[7 * 5 :]):
assert week in ("-----ww", "dddddww")
# Convert shift to a string representation if worker.name == "worker1":
shifts_string = "".join([i if i != "" else "-" for i in shifts]) assert shifts_string[: 7 * 5].count("ww-") == 4
assert shifts_string.count("d") == 25
assert shifts_string.count("w") == 20
def test_nwd_testing():
Rota = setup_rota()
start_date = Rota.start_date
d = start_date + datetime.timedelta(weeks=5)
worker1 = Worker(
name="worker1", site="group1", grade=1,
nwds=[
{"day": "Mon", "start_date": d, "end_date": Rota.rota_end_date},
{"day": "Tue", "start_date": d, "end_date": Rota.rota_end_date},
{"day": "Wed", "start_date": d, "end_date": Rota.rota_end_date},
{"day": "Thu", "start_date": d, "end_date": Rota.rota_end_date},
{"day": "Fri", "start_date": d, "end_date": Rota.rota_end_date},
],
)
worker2 = Worker(name="worker2", site="group1", grade=1)
Rota.add_workers((worker1, worker2))
Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length=12.5, days=days[:5],
balance_offset=10, workers_required=1,
force_as_block_unless_nwd=True, assign_as_block=True,
),
SingleShift(
sites=("group1",), name="w", length=12.5, days=days[5:],
balance_offset=10, workers_required=2,
force_as_block_unless_nwd=True, assign_as_block=True,
),
)
Rota.build_and_solve(options={"ratio": 0.00})
Rota.export_rota_to_html("nwd", folder="tests")
assert Rota.results.solver.status == "ok"
for worker in Rota.workers:
shifts = Rota.get_worker_shift_list(worker)
shifts_string = "".join([i if i != "" else "-" for i in shifts])
for week in weeks_from_list(shifts_string[7 * 5 :]):
if worker.name == "worker1": if worker.name == "worker1":
assert "wwd" not in shifts_string assert week == "-----ww"
else:
if worker.name == "worker2": assert week == "dddddww"
assert "dww" not in shifts_string for week in weeks_from_list(shifts_string[: 7 * 5]):
def test_nwd_simple_fail(self):
# Set up rota
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
self.Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
# Add a few workers
worker1 = Worker(name="worker1", site="group1", grade=1, nwds=[{"day": "Mon"}])
worker2 = Worker(name="worker2", site="group1", grade=1, nwds=[{"day": "Mon"}])
self.Rota.add_workers((worker1, worker2))
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=1,
),
)
self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("nwd")
assert self.Rota.results.solver.status in ("warning", "error")
def test_nwd_partial_rota(self):
# Set up rota
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
self.Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
d = start_date + datetime.timedelta(weeks=5)
# Add a few workers
worker1 = Worker(
name="worker1",
site="group1",
grade=1,
nwds=[{"day": "Mon", "start_date": start_date, "end_date": d}],
)
worker2 = Worker(
name="worker2",
site="group1",
grade=1,
nwds=[{"day": "Mon", "start_date": d, "end_date": self.Rota.rota_end_date}],
)
self.Rota.add_workers((worker1, worker2))
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=1,
),
)
self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("nwd")
assert self.Rota.results.solver.status == "ok"
for worker in self.Rota.workers:
shifts = self.Rota.get_worker_shift_list(worker)
# Convert shift to a string representation
shifts_string = "".join([i if i != "" else "-" for i in shifts])
if worker.name == "worker1": if worker.name == "worker1":
for n in range(0,4): assert week == "dddddww"
assert shifts_string[0 + 7*n] == "-" else:
assert week == "-----ww"
if worker.name == "worker2":
for n in range(5,9):
assert shifts_string[0 + 7*n] == "-"
def test_nwd_partial_overlay(self):
# Set up rota
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
self.Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
d = start_date + datetime.timedelta(weeks=5)
d2 = start_date + datetime.timedelta(weeks=6)
# Add a few workers
worker1 = Worker(
name="worker1",
site="group1",
grade=1,
nwds=[{"day":"Mon", "start_date":start_date, "end_date":d2}],
)
worker2 = Worker(
name="worker2",
site="group1",
grade=1,
nwds=[{"day":"Mon", "start_date":d, "end_date":self.Rota.rota_end_date}],
)
self.Rota.add_workers((worker1, worker2))
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=1,
),
)
self.Rota.build_and_solve(options={"ratio": 0.00})
assert self.Rota.results.solver.status in ("warning", "error")
def test_nwd_force_as_block(self):
# Set up rota
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
self.Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
d = start_date + datetime.timedelta(weeks=4)
worker1 = Worker(
name="worker1",
site="group1",
grade=1,
nwds=[{"day": "Mon", "start_date":start_date, "end_date":d}],
)
worker2 = Worker(name="worker2", site="group1", grade=1,
)
worker3 = Worker(name="worker3", site="group1", grade=1,
)
worker4 = Worker(name="worker4", site="group1", grade=1,
)
self.Rota.add_workers((worker1, worker2, worker3, worker4))
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=2,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
)
self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("nwd")
assert self.Rota.results.solver.status == "ok"
for worker in self.Rota.workers:
shifts = self.Rota.get_worker_shift_list(worker)
# Convert shift to a string representation
shifts_string = "".join([i if i != "" else "-" for i in shifts])
for week in weeks_from_list(shifts_string):
assert week in ("-------", "ddddd--")
assert shifts_string.count("d") == 25
def test_nwd_force_as_block_force_split(self):
# Set up rota
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
self.Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
d = start_date + datetime.timedelta(weeks=5)
worker1 = Worker(
name="worker1",
site="group1",
grade=1,
nwds=[{"day": "Mon", "start_date":start_date, "end_date":d}],
)
worker2 = Worker(name="worker2", site="group1", grade=1,
)
worker3 = Worker(name="worker3", site="group1", grade=1,
)
worker4 = Worker(name="worker4", site="group1", grade=1,
)
self.Rota.add_workers((worker1, worker2, worker3, worker4))
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length= 12.5, days=days[:5],
#balance_offset=50,
workers_required=2,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
SingleShift(
sites=("group1",), name="w", length= 12.5, days=days[5:],
#balance_offset=50,
workers_required=4,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
)
self.Rota.build_and_solve()
self.Rota.export_rota_to_html("nwd_block_force_split")
assert self.Rota.results.solver.status == "ok"
for worker in self.Rota.workers:
shifts = self.Rota.get_worker_shift_list(worker)
# Convert shift to a string representation
shifts_string = "".join([i if i != "" else "-" for i in shifts])
for week in weeks_from_list(shifts_string[7 * 5 :]):
assert week in ("-----ww", "dddddww")
if worker.name == "worker1":
assert shifts_string[: 7 * 5].count("ww-") == 4
assert shifts_string.count("d") == 25
assert shifts_string.count("w") == 20
def test_nwd_testing(self):
# Set up rota
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
self.Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
d = start_date + datetime.timedelta(weeks=5)
worker1 = Worker(name="worker1", site="group1", grade=1,
nwds=[
{"day": "Mon", "start_date":d, "end_date":self.Rota.rota_end_date},
{"day": "Tue", "start_date":d, "end_date":self.Rota.rota_end_date},
{"day": "Wed", "start_date":d, "end_date":self.Rota.rota_end_date},
{"day": "Thu", "start_date":d, "end_date":self.Rota.rota_end_date},
{"day": "Fri", "start_date":d, "end_date":self.Rota.rota_end_date},
],
)
worker2 = Worker(name="worker2", site="group1", grade=1,
)
self.Rota.add_workers((worker1, worker2))
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=1,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
SingleShift(
sites=("group1",), name="w", length= 12.5, days=days[5:],
balance_offset=10,
workers_required=2,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
)
self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("nwd")
assert self.Rota.results.solver.status == "ok"
for worker in self.Rota.workers:
shifts = self.Rota.get_worker_shift_list(worker)
# Convert shift to a string representation
shifts_string = "".join([i if i != "" else "-" for i in shifts])
for week in weeks_from_list(shifts_string[7 * 5 :]):
if worker.name == "worker1":
assert week == "-----ww"
else:
assert week == "dddddww"
for week in weeks_from_list(shifts_string[: 7 * 5]):
if worker.name == "worker1":
assert week == "dddddww"
else:
assert week == "-----ww"
+97 -1201
View File
File diff suppressed because it is too large Load Diff
+85
View File
@@ -0,0 +1,85 @@
import pytest
from rota.shifts import RotaBuilder, SingleShift, days
from rota.workers import Worker
import datetime
@pytest.fixture
def demo_rota_clear():
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1,
)
Rota.constraint_options["max_shifts_per_month"] = 20
workers = [
Worker(name="worker1", site="group1", grade=1),
Worker(name="worker2", site="group1", grade=1),
Worker(name="worker3", site="group1", grade=1),
Worker(name="worker4", site="group1", grade=1),
Worker(name="worker5", site="group2", grade=1, fte=60),
Worker(name="worker6", site="group2", grade=1, fte=40),
Worker(name="worker7", site="group2", grade=1),
Worker(name="worker8", site="group2", grade=1),
]
Rota.add_workers(workers)
return Rota
def test_pre(demo_rota_clear):
Rota = demo_rota_clear
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="a",
length=12.5,
days=days[:5],
workers_required=2,
constraint=[{"name": "pre", "options": 1}],
),
SingleShift(
sites=("group1", "group2"),
name="b",
length=12.5,
days=days[:5],
workers_required=2,
),
SingleShift(
sites=("group1", "group2"),
name="c",
length=12.5,
days=days[3],
workers_required=1,
constraint=[{"name": "pre", "options": 1}, {"name": "post", "options": 1}],
),
)
Rota.build_shifts()
Rota.build_workers()
Rota.build_model()
solver_options = {"seconds": 1000, "threads": 10}
Rota.solve_model(options=solver_options)
Rota.export_rota_to_html("test4")
assert Rota.results.solver.status == "ok"
assert Rota.results.solver.termination_condition == "optimal"
for worker in Rota.workers:
shifts = Rota.get_worker_shift_list(worker)
shifts_string = "".join([i if i != "" else "-" for i in shifts])
# check for patterns that should not occur
assert "ba" not in shifts_string
assert "ca" not in shifts_string
assert "ac" not in shifts_string
assert "ca" not in shifts_string
assert "bc" not in shifts_string
assert "cb" not in shifts_string
# Uncomment if you want to check for these as well:
# assert "a-c" not in shifts_string
# assert "c-a" not in shifts_string
# assert "b-c" not in shifts_string
# assert "c-b" not in shifts_string
+99
View File
@@ -0,0 +1,99 @@
import datetime
import pytest
from pytest import approx
from rota.shifts import RotaBuilder, SingleShift, days
from rota.workers import Worker
@pytest.fixture
def demo_rota_nights():
weeks_to_rota = 12
start_date = datetime.date(2022, 3, 14)
Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1,
)
Rota.constraint_options["max_shifts_per_week"] = 5
Rota.constraint_options["max_shifts_per_month"] = 31
Rota.constraint_options["limit_to_1_st2_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
Rota.constraint_options["max_night_frequency"] = 0
# Add workers
workers = [
Worker(name="worker1", site="group1", grade=1),
Worker(name="worker2", site="group1", grade=1),
Worker(name="worker3", site="group1", grade=1),
Worker(name="worker4", site="group1", grade=1),
Worker(name="worker5", site="group2", grade=1, fte=60),
Worker(name="worker6", site="group2", grade=1, fte=40),
Worker(name="worker7", site="group2", grade=1),
Worker(name="worker8", site="group2", grade=1),
]
Rota.add_workers(workers)
# Add shifts
Rota.add_shifts(
SingleShift(
sites=("group1", "group2"),
name="night_weekday",
length=12.5,
days=days[:4],
workers_required=1,
constraint=[{"name": "pre", "options": 1}, {"name": "post", "options": 2}],
force_as_block=True
),
SingleShift(
sites=("group1", "group2"),
name="night_weekend",
length=12.5,
days=days[4:],
constraint=[{"name": "pre", "options": 1}, {"name": "post", "options": 2}],
force_as_block=True,
),
SingleShift(
sites=("group1", "group2"),
name="twilight",
length=12.5,
days=days[:5],
workers_required=2,
),
)
Rota.build_shifts()
Rota.build_workers()
Rota.build_model()
solver_options = {"ratio": 0.01, "seconds": 1000, "threads": 10}
Rota.solve_model(options=solver_options)
Rota.export_rota_to_html("test3")
return Rota
def test_night_assignment(demo_rota_nights):
Rota = demo_rota_nights
assert Rota.results.solver.status == "ok"
shift_summary = Rota.get_shift_summary_dict()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
if worker.fte == 40:
assert worker_shifts["night_weekday"] in (approx(0), approx(4))
assert worker_shifts["night_weekend"] in (approx(0), approx(3))
elif worker.fte == 60:
assert worker_shifts["night_weekday"] in (approx(0), approx(4), approx(8))
assert worker_shifts["night_weekend"] in (approx(0), approx(3), approx(6))
else:
assert worker_shifts["night_weekday"] in (approx(4), approx(8))
assert worker_shifts["night_weekend"] in (approx(3), approx(6))
+71
View File
@@ -0,0 +1,71 @@
import pytest
import datetime
from copy import deepcopy
from rota.shifts import RotaBuilder, SingleShift, days
from rota.workers import Worker
@pytest.fixture
def rota_2workers():
weeks_to_rota = 10
start_date = datetime.date(2022, 3, 7)
Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1,
)
worker1 = Worker(name="worker1", site="group1", grade=1)
worker2 = Worker(name="worker2", site="group1", grade=1)
Rota.add_workers((worker1, worker2))
Rota.add_shifts(
SingleShift(
sites=("group1",),
name="a",
length=12.5,
days=days,
balance_offset=40,
workers_required=1,
),
)
return Rota
@pytest.fixture
def rota_3workers(rota_2workers):
Rota3 = deepcopy(rota_2workers)
worker3 = Worker(name="worker3", site="group1", grade=1)
Rota3.add_worker(worker3)
return Rota3
def test_max_shifts(rota_2workers):
rota_2workers.constraint_options["max_shifts_per_month"] = 14
rota_2workers.build_and_solve()
assert rota_2workers.results.solver.status == "ok"
assert rota_2workers.results.solver.termination_condition == "optimal"
def test_max_shifts_fail(rota_2workers):
rota_2workers.constraint_options["max_shifts_per_month"] = 13
rota_2workers.build_and_solve()
assert rota_2workers.results.solver.status in ("warning", "error")
assert rota_2workers.results.solver.termination_condition == "infeasible"
def test_max_shifts_extra_worker(rota_3workers):
rota_3workers.constraint_options["max_shifts_per_month"] = 12
rota_3workers.build_and_solve()
assert rota_3workers.results.solver.status == "ok"
assert rota_3workers.results.solver.termination_condition == "optimal"
def test_max_shifts_per_week_fail(rota_2workers):
rota_2workers.constraint_options["max_shifts_per_week"] = 3
rota_2workers.build_and_solve()
assert rota_2workers.results.solver.status in ("warning", "error")
assert rota_2workers.results.solver.termination_condition == "infeasible"
def test_max_shifts_per_week_pass(rota_2workers):
rota_2workers.constraint_options["max_shifts_per_month"] = 14
rota_2workers.constraint_options["max_shifts_per_week"] = 4
rota_2workers.build_and_solve()
assert rota_2workers.results.solver.status == "ok"
def test_max_shifts_per_week_extra_worker_pass(rota_3workers):
rota_3workers.constraint_options["max_shifts_per_week"] = 3
rota_3workers.build_and_solve()
assert rota_3workers.results.solver.status == "ok"