From 9d187dfa7536a444dab145af42806d2d0f52e385 Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 17 Dec 2025 12:39:11 +0000 Subject: [PATCH] Refactor weekend day balancing logic by commenting out unused option and storing adjusted targets on worker objects --- gen_cons.py | 2 +- requirements.txt | 5 ++- rota_generator/shifts.py | 75 +++++++++------------------------------- 3 files changed, 21 insertions(+), 61 deletions(-) mode change 100644 => 100755 requirements.txt diff --git a/gen_cons.py b/gen_cons.py index d71652b..8227009 100644 --- a/gen_cons.py +++ b/gen_cons.py @@ -488,7 +488,7 @@ def main( balance_weekends=True, max_shifts_per_week=6, max_days_per_week_block=[(4, 2), (4, 4)], - balance_weekend_days=True, + #balance_weekend_days=True, balance_weekend_days_quadratic=True, weekend_day_hard_max_deviation=2, balance_weekend_days_use_previous_shifts=True, diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 index b80a844..3bc19b2 --- a/requirements.txt +++ b/requirements.txt @@ -18,4 +18,7 @@ typer django>=6.0,<7 django-crispy-forms crispy-bulma -django-debug-toolbar \ No newline at end of file +django-debug-toolbar +pandas +odfpy +openpyxl \ No newline at end of file diff --git a/rota_generator/shifts.py b/rota_generator/shifts.py index 281abc5..69381ec 100644 --- a/rota_generator/shifts.py +++ b/rota_generator/shifts.py @@ -2596,7 +2596,10 @@ class RotaBuilder(object): except Exception: continue weekend_day_shift_target_number = weekend_day_shift_target_number + prev_adj - + # store adjusted per-day weekend target on the worker object for later use in objectives + if not hasattr(worker, 'weekend_day_target'): + worker.weekend_day_target = {} + worker.weekend_day_target[day_name] = weekend_day_shift_target_number self.model.constraints.add( self.model.weekend_day_shift_count_t1[worker.id, day_name] - self.model.weekend_day_shift_count_t2[worker.id, day_name] @@ -3587,64 +3590,18 @@ class RotaBuilder(object): weekend_day_term = 0 if self.constraint_options_model.balance_weekend_days_quadratic: wd_weight = self.constraint_options_model.balance_weekend_days_weight - # Quadratic penalty: strongly discourage deviations from per-day targets - weekend_day_term = 0 - for worker in self.workers: - for day_name in ("Sat", "Sun"): - # base target derived from shift targets distributed over shift days - base_target = sum( - worker.shift_target_number[shift.name] / len(shift.days) - for shift in self.get_shifts() - if day_name in shift.days - ) - - # optionally include previous_shifts adjustments into the per-day target - prev_adj = 0.0 - if self.use_previous_shifts and self.constraint_options_model.balance_weekend_days_use_previous_shifts: - for shift in self.get_shifts(): - if day_name in shift.days and shift.name in getattr(worker, "previous_shifts", {}): - entry = worker.previous_shifts.get(shift.name) - if isinstance(entry, dict): - # prefer explicit per-day entry - if day_name in entry: - try: - worked = float(entry[day_name][0]) - allocated = float(entry[day_name][1]) - prev_adj += (allocated - worked) - except Exception: - continue - elif "__all__" in entry: - try: - worked = float(entry["__all__"][0]) - allocated = float(entry["__all__"][1]) - prev_adj += (allocated - worked) / max(1, len(shift.days)) - except Exception: - continue - else: - # sum any tuple/list values found in the dict (spread over days) - try: - total_worked = 0.0 - total_alloc = 0.0 - for v in entry.values(): - if isinstance(v, (list, tuple)): - total_worked += float(v[0]) - total_alloc += float(v[1]) - prev_adj += (total_alloc - total_worked) / max(1, len(shift.days)) - except Exception: - continue - else: - try: - worked = float(entry[0]) - allocated = float(entry[1]) - prev_adj += (allocated - worked) / max(1, len(shift.days)) - except Exception: - continue - - adjusted_target = base_target + prev_adj - - weekend_day_term += wd_weight * ( - self.model.weekend_day_shift_count[(worker.id, day_name)] - adjusted_target - ) ** 2 + # HiGHS/appsi does not accept arbitrary nonlinear objective expressions here. + # Use the existing t1/t2 linear representation (which approximates absolute + # deviation) in the objective instead of a true squared term. + weekend_day_term = sum( + wd_weight + * ( + self.model.weekend_day_shift_count_t1[(worker.id, day_name)] + + self.model.weekend_day_shift_count_t2[(worker.id, day_name)] + ) + for worker in self.workers + for day_name in ("Sat", "Sun") + ) elif self.constraint_options_model.balance_weekend_days: weekend_day_modifier = self.constraint_options_model.balance_weekend_days_weight weekend_day_term = sum(