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
|
from collections import defaultdict
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
@@ -299,7 +300,7 @@ def load_workers():
|
|||||||
w.add_force_assign_with("weekend", "oncall")
|
w.add_force_assign_with("weekend", "oncall")
|
||||||
|
|
||||||
if w.name in ("DS",):
|
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:
|
else:
|
||||||
w.add_hard_day_dependency({"Fri", "Sun"})
|
w.add_hard_day_dependency({"Fri", "Sun"})
|
||||||
w.add_hard_day_exclusion({"Sat", "Sun"})
|
w.add_hard_day_exclusion({"Sat", "Sun"})
|
||||||
|
|||||||
@@ -1702,6 +1702,18 @@ class RotaBuilder(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for week in weeks_to_apply:
|
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,
|
# If the worker has any forced assignment in the dependency group,
|
||||||
# emit a concise warning. For visibility include every day in the
|
# emit a concise warning. For visibility include every day in the
|
||||||
# group showing the worker's forced shifts (if any) and other
|
# group showing the worker's forced shifts (if any) and other
|
||||||
@@ -1729,6 +1741,9 @@ class RotaBuilder(object):
|
|||||||
date_map_for_week = {}
|
date_map_for_week = {}
|
||||||
|
|
||||||
for day in day_group:
|
for day in day_group:
|
||||||
|
if day in ignored_days:
|
||||||
|
# skip ignored dates for warning/visibility
|
||||||
|
continue
|
||||||
worker_forced_shifts = set()
|
worker_forced_shifts = set()
|
||||||
# week-based forced assignments
|
# week-based forced assignments
|
||||||
for (fw, fd, fs) in getattr(worker, "forced_assignments", []):
|
for (fw, fd, fs) in getattr(worker, "forced_assignments", []):
|
||||||
@@ -1799,8 +1814,14 @@ class RotaBuilder(object):
|
|||||||
"HardDayDependency forced assignment",
|
"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.",
|
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 = {}
|
assigned_any_shift = {}
|
||||||
for day in day_group:
|
for day in effective_days:
|
||||||
shifts_today = self.get_shift_names_by_week_day(week, day)
|
shifts_today = self.get_shift_names_by_week_day(week, day)
|
||||||
var_name = f"hard_day_dep_{worker.id}_{week}_{day}"
|
var_name = f"hard_day_dep_{worker.id}_{week}_{day}"
|
||||||
if not hasattr(self.model, "hard_day_dep_vars"):
|
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)
|
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 >= assigned_any_shift[day])
|
||||||
self.model.constraints.add(total_assigned <= len(shifts_today) * 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())
|
vals = list(assigned_any_shift.values())
|
||||||
for i in range(1, len(vals)):
|
for i in range(1, len(vals)):
|
||||||
self.model.constraints.add(vals[i] == vals[0])
|
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
|
weeks: Optional[List[int]] = None # If None, applies to all weeks
|
||||||
start_date: Optional[datetime.date] = None # Optional start date for the dependency
|
start_date: Optional[datetime.date] = None # Optional start date for the dependency
|
||||||
end_date: Optional[datetime.date] = None # Optional end 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")
|
@field_validator("weeks")
|
||||||
def weeks_none_if_dates_set(cls, weeks, values):
|
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 = []
|
||||||
self.allowed_multi_shift_sets.append(frozenset(shifts))
|
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.
|
Add a hard day dependency for this worker.
|
||||||
days: List of days that must be grouped together.
|
days: List of days that must be grouped together.
|
||||||
weeks: Optional list of weeks this dependency applies to. If None, applies to all weeks.
|
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):
|
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