Enhance hard day dependency handling with ignore dates and improve warning visibility
This commit is contained in:
+2
-1
@@ -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"})
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user