Enhance hard day dependency handling with ignore dates and improve warning visibility

This commit is contained in:
Ross
2025-12-16 22:41:43 +00:00
parent 51ed25d04c
commit 108bb572a7
3 changed files with 28 additions and 5 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
from pyomo.opt.results.container import ignore
from collections import defaultdict
import datetime
import os
@@ -299,7 +300,7 @@ def load_workers():
w.add_force_assign_with("weekend", "oncall")
if w.name in ("DS",):
w.add_hard_day_dependency({"Fri", "Sat", "Sun"})
w.add_hard_day_dependency({"Fri", "Sat", "Sun"}, ignore_dates=[datetime.date(2026, 5, 2)])
else:
w.add_hard_day_dependency({"Fri", "Sun"})
w.add_hard_day_exclusion({"Sat", "Sun"})
+23 -2
View File
@@ -1702,6 +1702,18 @@ class RotaBuilder(object):
)
for week in weeks_to_apply:
# compute which days in the day_group should be ignored
ignored_days = set()
try:
for ignore_date in getattr(dep, "ignore_dates", []) or []:
try:
ww, dd = self.date_week_day_map.get(ignore_date, (None, None))
except Exception:
ww = dd = None
if ww == week and dd in day_group:
ignored_days.add(dd)
except Exception:
ignored_days = set()
# If the worker has any forced assignment in the dependency group,
# emit a concise warning. For visibility include every day in the
# group showing the worker's forced shifts (if any) and other
@@ -1729,6 +1741,9 @@ class RotaBuilder(object):
date_map_for_week = {}
for day in day_group:
if day in ignored_days:
# skip ignored dates for warning/visibility
continue
worker_forced_shifts = set()
# week-based forced assignments
for (fw, fd, fs) in getattr(worker, "forced_assignments", []):
@@ -1799,8 +1814,14 @@ class RotaBuilder(object):
"HardDayDependency forced assignment",
f"Worker '{worker.name}' has hard_day_dependency {set(day_group)} in week {week}; forced assignments on dependency days: {details}. If the rota is not infeasible this is unlikely to be an issue.",
)
# Build constraints only for non-ignored days
effective_days = [d for d in day_group if d not in ignored_days]
if not effective_days:
# nothing to constrain for this dependency in this week
continue
assigned_any_shift = {}
for day in day_group:
for day in effective_days:
shifts_today = self.get_shift_names_by_week_day(week, day)
var_name = f"hard_day_dep_{worker.id}_{week}_{day}"
if not hasattr(self.model, "hard_day_dep_vars"):
@@ -1812,7 +1833,7 @@ class RotaBuilder(object):
total_assigned = sum(self.model.works[worker.id, week, day, shift] for shift in shifts_today)
self.model.constraints.add(total_assigned >= assigned_any_shift[day])
self.model.constraints.add(total_assigned <= len(shifts_today) * assigned_any_shift[day])
# All-or-none: all days in the group must have the same assignment status
# All-or-none: all effective (non-ignored) days in the group must have the same assignment status
vals = list(assigned_any_shift.values())
for i in range(1, len(vals)):
self.model.constraints.add(vals[i] == vals[0])
+3 -2
View File
@@ -36,6 +36,7 @@ class HardDayDependency(BaseModel):
weeks: Optional[List[int]] = None # If None, applies to all weeks
start_date: Optional[datetime.date] = None # Optional start date for the dependency
end_date: Optional[datetime.date] = None # Optional end date for the dependency
ignore_dates: list[datetime.date] = Field(default_factory=list, description="List of specific dates to ignore for this dependency")
@field_validator("weeks")
def weeks_none_if_dates_set(cls, weeks, values):
@@ -437,13 +438,13 @@ class Worker(BaseModel):
self.allowed_multi_shift_sets = []
self.allowed_multi_shift_sets.append(frozenset(shifts))
def add_hard_day_dependency(self, days: set[str], weeks: Optional[List[int]] = None, start_date: Optional[datetime.date] = None, end_date: Optional[datetime.date] = None):
def add_hard_day_dependency(self, days: set[str], weeks: Optional[List[int]] = None, start_date: Optional[datetime.date] = None, end_date: Optional[datetime.date] = None, ignore_dates: Optional[List[datetime.date]] = None):
"""
Add a hard day dependency for this worker.
days: List of days that must be grouped together.
weeks: Optional list of weeks this dependency applies to. If None, applies to all weeks.
"""
self.hard_day_dependencies.append(HardDayDependency(days=days, weeks=weeks, start_date=start_date, end_date=end_date))
self.hard_day_dependencies.append(HardDayDependency(days=days, weeks=weeks, start_date=start_date, end_date=end_date, ignore_dates=ignore_dates or []))
def add_hard_day_exclusion(self, days: set[str], weeks: Optional[List[int]] = None, start_date: Optional[datetime.date] = None, end_date: Optional[datetime.date] = None):
"""