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,
):
console.print("Setting up solver")
console.print(f"{options=}")
if solver == "scip":
if "seconds" in options:
@@ -516,6 +517,9 @@ class RotaBuilder(object):
self.build_workers()
self.build_model()
console.print("Building model")
console.print(f"{options=}")
if solve:
self.solve_model(
options=options, debug_if_fail=debug_if_fail, solver=solver
@@ -2203,7 +2207,6 @@ class RotaBuilder(object):
self.get_week_start_date(week)
):
# Force shifts to be assigned in blocks
try:
self.model.constraints.add(
self.model.shift_week_worker_assigned[
shift.name, week, worker.id
@@ -2214,8 +2217,6 @@ class RotaBuilder(object):
for day in shift.days
)
)
except KeyError:
pass # blocks with workerd requirements
#
@@ -3213,7 +3214,7 @@ class RotaBuilder(object):
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):
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 pytest
from pytest import approx
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
from rota.shifts import RotaBuilder, SingleShift, days
from rota.workers import Worker
def generate_basic_rota(weeks_to_rota=10):
start_date = datetime.date(2022, 3, 7)
Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
Rota.constraint_options["balance_weekends"] = True
# Add a few workers
Rota.add_workers(
[
Worker(name="worker1", site="group1", grade=1),
Worker(name="worker2", site="group1", 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
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_basic_balancing(self):
Rota = generate_basic_rota()
def test_weighted_shift_balancing():
Rota = generate_basic_rota(20)
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(
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,
),
)
def test_weighted_shift_balancing2():
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=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]
assert worker_shifts["b"] in (approx(53), approx(54), approx(55))
Rota.build_and_solve(options={"ratio": 0.001})
Rota.export_rota_to_html("basic_balancing")
def test_weighted_shift_balancing3():
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()
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
def test_weighted_shift_balancing4():
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,
),
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)
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))
def test_weighted_shift_balancing(self):
Rota = generate_basic_rota(20)
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]
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)
def test_weighted_shift_balancing5():
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,
workers_required=2,
force_as_block=False,
),
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]
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 pytest
from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, days
from rota.workers import Worker
def generate_basic_rota(weeks_to_rota=10):
start_date = datetime.date(2022, 3, 7)
Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
# Add a few workers
Rota.add_workers(
[
Worker(name="worker1", site="group1", grade=1),
Worker(name="worker2", site="group1", grade=1),
]
)
return Rota
def date_generator(from_date, days):
n = 0
while True:
yield from_date
n = n + 1
if n >= days:
break
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(self):
Rota = generate_basic_rota()
def test_nights_pre3():
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(
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,
# constraint=[{"name": "night"}],
),
)
def test_nights_pre3_2():
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,
),
)
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})
Rota.export_rota_to_html("night")
def test_nights_fail():
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"
assert Rota.results.solver.termination_condition == "optimal"
def test_nights2():
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):
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,
# 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_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},
def test_nights_pre_wrap_around():
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,
),
)
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",
},
],
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.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night pre wrap around", folder="tests")
assert Rota.results.solver.status == "ok"
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",
# },
],
),
]
)
def test_nights_max_frequency():
Rota = generate_basic_rota()
Rota.constraint_options["max_night_frequency"] = 1
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", folder="tests")
assert Rota.results.solver.status == "ok"
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
def test_nights_max_frequency_fail():
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):
Rota = generate_basic_rota()
def test_nights_max_frequency4():
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(
SingleShift(
sites=("group1", "group2"),
name="n",
length=12.5,
days=days[5:],
force_as_block=True,
constraint=[{"name": "night"}],
workers_required=2,
def test_nights_max_frequency_exclusions2():
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)
),
# 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_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,
Worker(
name="worker4",
site="group1",
grade=1,
start_date=Rota.start_date + datetime.timedelta(weeks=4)
),
# 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 max frequency exclusions 2", 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_frequency3(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,
def test_nights_max_frequency_exclusions3():
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)
),
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,
Worker(
name="worker4",
site="group1",
grade=1,
start_date=Rota.start_date + datetime.timedelta(weeks=4)
),
# 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 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"
]
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night max frequency exclusions 3", folder="tests")
assert Rota.results.solver.status == "ok"
+195 -353
View File
@@ -1,374 +1,216 @@
import pytest
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
import datetime
from rota.workers import Worker
def weeks_from_list(lst):
"""Yield successive n-sized chunks from a lst."""
for i in range(0, len(lst), 7):
yield lst[i : i + 7]
class TestDemoRota:
def test_nwd(self):
# Set up rota
weeks_to_rota = 10
def setup_rota(weeks_to_rota=10, start_date=None):
if start_date is None:
start_date = datetime.date(2022, 3, 7)
return RotaBuilder(start_date, weeks_to_rota=weeks_to_rota)
self.Rota = RotaBuilder(
start_date,
weeks_to_rota=weeks_to_rota,
)
def test_nwd():
Rota = setup_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
worker1 = Worker(name="worker1", site="group1", grade=1, nwds=[{"day": "Mon"}])
worker2 = Worker(name="worker2", site="group1", grade=1, nwds=[{"day": "Fri"}])
def test_nwd_simple_fail():
Rota = setup_rota()
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
self.Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length= 12.5, days=days[:5],
# balance_offset=10,
workers_required=1,
),
SingleShift(
sites=("group1",), name="w", length= 12.5, days=days[5:],
workers_required=2,
),
)
def test_nwd_partial_overlay():
Rota = setup_rota()
start_date = Rota.start_date
d = start_date + datetime.timedelta(weeks=5)
d2 = start_date + datetime.timedelta(weeks=6)
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": 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 self.Rota.results.solver.termination_condition == "optimal"
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])
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 :]):
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():
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":
assert "wwd" not in shifts_string
if worker.name == "worker2":
assert "dww" not in shifts_string
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])
assert week == "-----ww"
else:
assert week == "dddddww"
for week in weeks_from_list(shifts_string[: 7 * 5]):
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] == "-"
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"
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"