start switching constraint options

This commit is contained in:
Ross
2025-12-09 14:09:41 +00:00
parent 05edb93427
commit 069da08f5a
+124 -157
View File
@@ -321,49 +321,51 @@ class SingleShift(BaseModel):
return self.name[0] return self.name[0]
class RotaConstraintOptions: class RotaConstraintOptions(BaseModel):
"""Class to hold rota-level constraint options""" """Pydantic model holding rota-level constraint options with types, defaults and descriptions.
def __init__(self): Validate/coerce raw JSON (e.g. from DB) via `RotaConstraintOptions.model_validate(raw)`.
# self.options: { name : (default_value, type, description) } Use `model_dump()` to produce a JSON-serializable dict for storage.
# For each option we keep a tuple: (default_value, python_type, description) """
# The types are informational and can be used when generating forms/docs.
self.constraint_options = { model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
"balance_nights": (True, bool, "Balance night shifts across workers"),
"constrain_time_off_after_nights": (False, bool, "Apply time-off constraints after night shifts"), balance_nights: bool = Field(True, description="Balance night shifts across workers")
"balance_nights_across_sites": (True, bool, "Balance night shifts across sites"), constrain_time_off_after_nights: bool = Field(False, description="Apply time-off constraints after night shifts")
"balance_bank_holidays": (True, bool, "Balance bank-holiday shifts across staff"), balance_nights_across_sites: bool = Field(True, description="Balance night shifts across sites")
"balance_blocks": (True, bool, "Balance shifts across week-blocks"), balance_bank_holidays: bool = Field(True, description="Balance bank-holiday shifts across staff")
"balance_shifts": (True, bool, "Balance shifts across workers"), balance_blocks: bool = Field(True, description="Balance shifts across week-blocks")
"balance_shifts_true_quadratic": (False, bool, "Use true quadratic balancing for shifts"), balance_shifts: bool = Field(True, description="Balance shifts across workers")
"balance_shifts_quadratic": (False, bool, "Use quadratic penalty to discourage spread across shifts"), balance_shifts_true_quadratic: bool = Field(False, description="Use true quadratic balancing for shifts")
"balance_shifts_over_workers": (True, bool, "Balance shift distribution over workers"), balance_shifts_quadratic: bool = Field(False, description="Use quadratic penalty to discourage spread across shifts")
"minimise_shift_diffs": (False, bool, "Use simpler minimisation to reduce shift differences"), balance_shifts_over_workers: bool = Field(True, description="Balance shift distribution over workers")
"maximum_allowed_shift_diff": (None, Optional[int], "Maximum allowed per-worker shift difference (None = no limit)"), minimise_shift_diffs: bool = Field(False, description="Use simpler minimisation to reduce shift differences")
"balance_weekends": (True, bool, "Balance weekend allocations"), maximum_allowed_shift_diff: Optional[int] = Field(None, description="Maximum allowed per-worker shift difference (None = no limit)")
"max_weekends": (100, int, "Maximum number of weekends considered"), balance_weekends: bool = Field(True, description="Balance weekend allocations")
"max_weekend_frequency": (1, int, "Don't assign multiple shifts every N weeks (requires balance_weekends)"), max_weekends: int = Field(100, description="Maximum number of weekends considered")
"max_night_frequency": (1, int, "Don't assign multiple night shifts every N weeks"), max_weekend_frequency: int = Field(1, description="Don't assign multiple shifts every N weeks (requires balance_weekends)")
"max_night_frequency_week_exclusions": ([], list, "Weeks to exclude from night frequency checks"), max_night_frequency: int = Field(1, description="Don't assign multiple night shifts every N weeks")
"max_shifts_per_week": (7, int, "Max shifts per worker per week"), max_night_frequency_week_exclusions: List[int] = Field(default_factory=list, description="Weeks to exclude from night frequency checks")
"max_shifts_per_month": (40, int, "Max shifts per worker per month"), max_shifts_per_week: int = Field(7, description="Max shifts per worker per week")
"max_days_per_week_block": ([], list, "Optional list of (max_days, week_block) tuples to limit days in blocks"), max_shifts_per_month: int = Field(40, description="Max shifts per worker per month")
"prevent_monday_after_full_weekends": ([], list, "Prevent assignment on Monday after full weekends"), max_days_per_week_block: List[tuple] = Field(default_factory=list, description="Optional list of (max_days, week_block) tuples to limit days in blocks")
"prevent_monday_and_tuesday_after_full_weekends": ([], list, "Prevent Monday/Tuesday after full weekends"), prevent_monday_after_full_weekends: List[int] = Field(default_factory=list, description="Prevent assignment on Monday after full weekends")
"prevent_fridays_before_full_weekends": ([], list, "Prevent Fridays before full weekends"), prevent_monday_and_tuesday_after_full_weekends: List[int] = Field(default_factory=list, description="Prevent Monday/Tuesday after full weekends")
"prevent_thursdays_before_full_weekends": ([], list, "Prevent Thursdays before full weekends"), prevent_fridays_before_full_weekends: List[int] = Field(default_factory=list, description="Prevent Fridays before full weekends")
"hard_constrain_pair_separation": (False, bool, "Enforce hard pair separation constraint"), prevent_thursdays_before_full_weekends: List[int] = Field(default_factory=list, description="Prevent Thursdays before full weekends")
"avoid_shifts_by_grades": ([], list, "List of (grade, shift_name) pairs or rules to avoid assigning a grade to a shift"), hard_constrain_pair_separation: bool = Field(False, description="Enforce hard pair separation constraint")
"avoid_shifts_by_worker_names": ([], list, "List of worker names (or patterns) to avoid assigning to particular shifts"), # These are lists of dicts with keys like {"grades": [...], "weeks": [...], "shifts": [...]}
"allocate_locum_shifts": (True, bool, "Allow allocation of locum shifts"), avoid_shifts_by_grades: List[dict] = Field(default_factory=list, description="List of rules to avoid assigning grades to shifts")
"distribute_locum_shifts": (True, bool, "Distribute locum shifts among available workers"), avoid_shifts_by_worker_names: List[dict] = Field(default_factory=list, description="List of rules to avoid assigning specific workers to shifts")
"balance_locum_shifts": (False, bool, "Balance locum shifts similarly to normal shifts"), allocate_locum_shifts: bool = Field(True, description="Allow allocation of locum shifts")
"maximum_allowed_locum_shifts_per_worker": (None, Optional[int], "Maximum locum shifts allowed per worker (None = no limit)"), distribute_locum_shifts: bool = Field(True, description="Distribute locum shifts among available workers")
"minimum_allowed_locum_shifts_per_worker": (None, Optional[int], "Minimum locum shifts required per worker (None = no minimum)"), balance_locum_shifts: bool = Field(False, description="Balance locum shifts similarly to normal shifts")
# This is a typed list of Pydantic constraint models; items should follow maximum_allowed_locum_shifts_per_worker: Optional[int] = Field(None, description="Maximum locum shifts allowed per worker (None = no limit)")
# the schema of MinSummedGradeByShiftsPerDayConstraint minimum_allowed_locum_shifts_per_worker: Optional[int] = Field(None, description="Minimum locum shifts required per worker (None = no minimum)")
"min_summed_grade_by_shifts_per_day": ([], List[MinSummedGradeByShiftsPerDayConstraint], "List of minimum-summed-grade constraints per day"), min_summed_grade_by_shifts_per_day: List[MinSummedGradeByShiftsPerDayConstraint] = Field(default_factory=list, description="List of minimum-summed-grade constraints per day")
}
# (Removed legacy dict-like convenience methods to enforce
# attribute access on the typed `RotaConstraintOptions` model.)
class RotaBuilder(object): class RotaBuilder(object):
"""Class to hold and manipulate shifts""" """Class to hold and manipulate shifts"""
@@ -412,45 +414,21 @@ class RotaBuilder(object):
self.balance_offset_modifier = balance_offset_modifier self.balance_offset_modifier = balance_offset_modifier
self.ltft_balance_offset = ltft_balance_offset self.ltft_balance_offset = ltft_balance_offset
self.constraint_options = { # Initialize constraint options with typed Pydantic model. Accept either
"balance_nights": True, # None (use defaults), a dict (raw values), or an existing model instance.
"constrain_time_off_after_nights": False, # Store the model instance in `self.constraint_options_model` and also
"balance_nights_across_sites": True, # expose it as `self.constraint_options` for minimal code changes.
"balance_bank_holidays": True, constraint_options = None
"balance_blocks": True, if constraint_options is None:
"balance_shifts": True, # Does not use a quadratic function opts_model = RotaConstraintOptions()
"balance_shifts_true_quadratic": False, # Does not use a quadratic function elif isinstance(constraint_options, RotaConstraintOptions):
"balance_shifts_quadratic": False, # Will prevent spreading of spreading across different shifts opts_model = constraint_options
"balance_shifts_over_workers": True, elif isinstance(constraint_options, dict):
"minimise_shift_diffs": False, # less sophisticated version of balance_shifts_over_workers opts_model = RotaConstraintOptions.model_validate(constraint_options)
"maximum_allowed_shift_diff": None, else:
"balance_weekends": True, opts_model = RotaConstraintOptions()
"max_weekends": 100,
# Don't assign multiple shifts every (n) weeks self.constraint_options_model: RotaConstraintOptions = opts_model
"max_weekend_frequency": 1, # Requires balance weekends
# Don't assign multiple shifts every (n) weeks
"max_night_frequency": 1,
"max_night_frequency_week_exclusions": [],
"max_shifts_per_week": 7,
"max_shifts_per_month": 40,
"max_days_per_week_block": [],
#"max_days_per_week_block": [(3, 2)], # (max_days, week_block)
# The following will cause an unsolvable problem if a shift
# is forced as a block and spans the time peroid
"prevent_monday_after_full_weekends": [],
"prevent_monday_and_tuesday_after_full_weekends": [],
"prevent_fridays_before_full_weekends": [],
"prevent_thursdays_before_full_weekends": [],
"hard_constrain_pair_separation": False,
"avoid_shifts_by_grades": [],
"avoid_shifts_by_worker_names": [],
"allocate_locum_shifts": True,
"distribute_locum_shifts": True,
"balance_locum_shifts": False,
"maximum_allowed_locum_shifts_per_worker": None,
"minimum_allowed_locum_shifts_per_worker": None,
}
self.constraint_options["min_summed_grade_by_shifts_per_day"]: List[MinSummedGradeByShiftsPerDayConstraint] = []
self.terminate_on_warning = [ self.terminate_on_warning = [
"Worker/duplicate id", "Worker/duplicate id",
@@ -486,10 +464,11 @@ class RotaBuilder(object):
self.MAX_SHIFTS_PER_DAY = 2 self.MAX_SHIFTS_PER_DAY = 2
def set_rota_constraint(self, constraint: str, value: Any): def set_rota_constraint(self, constraint: str, value: Any):
if constraint not in self.constraint_options: # Ensure the model has the attribute and set it on the typed model
if not hasattr(self.constraint_options_model, constraint):
raise ValueError(f"Constraint {constraint} not valid") raise ValueError(f"Constraint {constraint} not valid")
self.constraint_options[constraint] = value setattr(self.constraint_options_model, constraint, value)
def set_rota_dates(self, start_date: datetime.date, weeks_to_rota: int): def set_rota_dates(self, start_date: datetime.date, weeks_to_rota: int):
self.weeks_to_rota = weeks_to_rota self.weeks_to_rota = weeks_to_rota
@@ -738,7 +717,7 @@ class RotaBuilder(object):
initialize=0, initialize=0,
) )
if self.constraint_options["balance_bank_holidays"]: if self.constraint_options_model.balance_bank_holidays:
# Bank holidays # Bank holidays
self.model.bank_holiday_count = Var( self.model.bank_holiday_count = Var(
((worker.id) for worker in self.workers), ((worker.id) for worker in self.workers),
@@ -769,7 +748,7 @@ class RotaBuilder(object):
# Used to limit number of workers on night shift per site # Used to limit number of workers on night shift per site
# if we force a binary it will in effect hard constrain to < 2 # if we force a binary it will in effect hard constrain to < 2
# from a single site # from a single site
if self.constraint_options["balance_nights_across_sites"]: if self.constraint_options_model.balance_nights_across_sites:
self.model.night_per_site = Var( self.model.night_per_site = Var(
( (
(week, shift.name, site) (week, shift.name, site)
@@ -864,7 +843,7 @@ class RotaBuilder(object):
initialize=0, initialize=0,
) )
if self.constraint_options["balance_shifts_over_workers"]: if self.constraint_options_model.balance_shifts_over_workers:
self.model.worker_shift_count_t1 = Var( self.model.worker_shift_count_t1 = Var(
((worker.id) for worker in self.workers), ((worker.id) for worker in self.workers),
domain=NonNegativeReals, domain=NonNegativeReals,
@@ -878,8 +857,8 @@ class RotaBuilder(object):
) )
if ( if (
self.constraint_options["balance_shifts"] self.constraint_options_model.balance_shifts
or self.constraint_options["balance_shifts_quadratic"] or self.constraint_options_model.balance_shifts_quadratic
): ):
self.model.shift_count_t1 = Var( self.model.shift_count_t1 = Var(
( (
@@ -901,7 +880,7 @@ class RotaBuilder(object):
initialize=0, initialize=0,
) )
if self.constraint_options["balance_shifts_quadratic"]: if self.constraint_options_model.balance_shifts_quadratic:
self.model.shift_count_w = Var( self.model.shift_count_w = Var(
( (
(worker.id, shift.name) (worker.id, shift.name)
@@ -912,7 +891,7 @@ class RotaBuilder(object):
initialize=0, initialize=0,
) )
if self.constraint_options["balance_nights"]: if self.constraint_options_model.balance_nights:
# We also try to even out the night shifts seperately # We also try to even out the night shifts seperately
self.model.night_shift_count = Var( self.model.night_shift_count = Var(
((worker.id) for worker in self.workers), ((worker.id) for worker in self.workers),
@@ -985,7 +964,7 @@ class RotaBuilder(object):
bounds=self.SHIFT_BOUNDS["weekend_count"], bounds=self.SHIFT_BOUNDS["weekend_count"],
) )
if self.constraint_options["balance_weekends"]: if self.constraint_options_model.balance_weekends:
self.model.weekend_shift_count_t1 = Var( self.model.weekend_shift_count_t1 = Var(
((worker.id) for worker in self.workers), ((worker.id) for worker in self.workers),
domain=NonNegativeReals, domain=NonNegativeReals,
@@ -1193,7 +1172,7 @@ class RotaBuilder(object):
self.model.constraints = ConstraintList() # Create a set of constraints self.model.constraints = ConstraintList() # Create a set of constraints
for constraint in self.constraint_options.get("min_summed_grade_by_shifts_per_day", []): for constraint in self.constraint_options_model.min_summed_grade_by_shifts_per_day:
weeks = constraint.weeks if constraint.weeks is not None else self.weeks weeks = constraint.weeks if constraint.weeks is not None else self.weeks
days_list = constraint.days if constraint.days is not None else self.days days_list = constraint.days if constraint.days is not None else self.days
for week in weeks: for week in weeks:
@@ -1608,7 +1587,7 @@ class RotaBuilder(object):
# Use per-worker value if set, otherwise use global constraint option if set # Use per-worker value if set, otherwise use global constraint option if set
max_days_worked_per_week = getattr(worker, "max_days_worked_per_week", None) max_days_worked_per_week = getattr(worker, "max_days_worked_per_week", None)
if max_days_worked_per_week is None: if max_days_worked_per_week is None:
max_days_worked_per_week = self.constraint_options.get("max_days_worked_per_week", None) max_days_worked_per_week = getattr(self.constraint_options_model, "max_days_worked_per_week", None)
if max_days_worked_per_week is not None: if max_days_worked_per_week is not None:
for week in self.weeks: for week in self.weeks:
# Create a binary variable for each day to indicate if the worker works any shift that day # Create a binary variable for each day to indicate if the worker works any shift that day
@@ -1834,7 +1813,7 @@ class RotaBuilder(object):
) )
) )
if self.constraint_options["distribute_locum_shifts"]: if self.constraint_options_model.distribute_locum_shifts:
self.model.constraints.add( self.model.constraints.add(
sum( sum(
self.model.locum_required[week, day, shift] self.model.locum_required[week, day, shift]
@@ -1845,32 +1824,26 @@ class RotaBuilder(object):
) )
if ( if (
self.constraint_options["maximum_allowed_locum_shifts_per_worker"] self.constraint_options_model.maximum_allowed_locum_shifts_per_worker
is not None is not None
): ):
self.model.constraints.add( self.model.constraints.add(
self.constraint_options[ self.constraint_options_model.maximum_allowed_locum_shifts_per_worker
"maximum_allowed_locum_shifts_per_worker"
]
>= self.model.locum_shifts_by_worker[worker.id] >= self.model.locum_shifts_by_worker[worker.id]
) )
if worker.locum_availability: if worker.locum_availability:
if ( if (
self.constraint_options[ self.constraint_options_model.minimum_allowed_locum_shifts_per_worker
"minimum_allowed_locum_shifts_per_worker"
]
is not None is not None
): ):
self.model.constraints.add( self.model.constraints.add(
self.constraint_options[ self.constraint_options_model.minimum_allowed_locum_shifts_per_worker
"minimum_allowed_locum_shifts_per_worker"
]
<= self.model.locum_shifts_by_worker[worker.id] <= self.model.locum_shifts_by_worker[worker.id]
) )
if ( if (
self.constraint_options["minimise_shift_diffs"] self.constraint_options_model.minimise_shift_diffs
or self.constraint_options["maximum_allowed_shift_diff"] is not None or self.constraint_options_model.maximum_allowed_shift_diff is not None
): ):
self.model.constraints.add( self.model.constraints.add(
self.model.shift_count_diff_summed[worker.id] self.model.shift_count_diff_summed[worker.id]
@@ -1880,17 +1853,17 @@ class RotaBuilder(object):
) )
) )
if self.constraint_options["maximum_allowed_shift_diff"] is not None: if self.constraint_options_model.maximum_allowed_shift_diff is not None:
self.model.constraints.add( self.model.constraints.add(
self.model.shift_count_diff_summed[worker.id] self.model.shift_count_diff_summed[worker.id]
<= self.constraint_options["maximum_allowed_shift_diff"] <= self.constraint_options_model.maximum_allowed_shift_diff
) )
self.model.constraints.add( self.model.constraints.add(
self.model.shift_count_diff_summed[worker.id] self.model.shift_count_diff_summed[worker.id]
>= -self.constraint_options["maximum_allowed_shift_diff"] >= -self.constraint_options_model.maximum_allowed_shift_diff
) )
for max_days, weeks in self.constraint_options["max_days_per_week_block"]: for max_days, weeks in self.constraint_options_model.max_days_per_week_block:
for week_blocks in self.get_week_block_iterator(weeks): for week_blocks in self.get_week_block_iterator(weeks):
self.model.constraints.add( self.model.constraints.add(
sum( sum(
@@ -1905,7 +1878,7 @@ class RotaBuilder(object):
# Prevent more than n number shifts per 4 weeks # Prevent more than n number shifts per 4 weeks
try: try:
self.model.constraints.add( self.model.constraints.add(
self.constraint_options["max_shifts_per_month"] self.constraint_options_model.max_shifts_per_month
>= sum( >= sum(
self.model.works[worker.id, week, day, shiftname] self.model.works[worker.id, week, day, shiftname]
for week, day, shiftname in self.get_all_shiftname_combinations() for week, day, shiftname in self.get_all_shiftname_combinations()
@@ -1921,9 +1894,7 @@ class RotaBuilder(object):
) )
pass pass
for constraint_dict in self.constraint_options[ for constraint_dict in self.constraint_options_model.avoid_shifts_by_worker_names:
"avoid_shifts_by_worker_names"
]:
if invalid_shifts := set(constraint_dict["shifts"]).difference( if invalid_shifts := set(constraint_dict["shifts"]).difference(
set(self.get_shift_names()) set(self.get_shift_names())
): ):
@@ -1945,7 +1916,7 @@ class RotaBuilder(object):
) )
) )
for constraint_dict in self.constraint_options["avoid_shifts_by_grades"]: for constraint_dict in self.constraint_options_model.avoid_shifts_by_grades:
if invalid_shifts := set(constraint_dict["shifts"]).difference( if invalid_shifts := set(constraint_dict["shifts"]).difference(
set(self.get_shift_names()) set(self.get_shift_names())
): ):
@@ -1980,9 +1951,9 @@ class RotaBuilder(object):
== sum(self.model.works_weekend[worker.id, week] for week in self.weeks) == sum(self.model.works_weekend[worker.id, week] for week in self.weeks)
) )
if self.constraint_options["max_weekends"] > -1: if self.constraint_options_model.max_weekends > -1:
self.model.constraints.add( self.model.constraints.add(
self.constraint_options["max_weekends"] self.constraint_options_model.max_weekends
>= self.model.worker_weekend_count[worker.id] >= self.model.worker_weekend_count[worker.id]
) )
@@ -2105,14 +2076,14 @@ class RotaBuilder(object):
- worker.shift_target_number[shift.name] - worker.shift_target_number[shift.name]
) )
if self.constraint_options["balance_shifts"]: if self.constraint_options_model.balance_shifts:
self.model.constraints.add( self.model.constraints.add(
self.model.shift_count_t1[worker.id, shift.name] self.model.shift_count_t1[worker.id, shift.name]
- self.model.shift_count_t2[worker.id, shift.name] - self.model.shift_count_t2[worker.id, shift.name]
== self.model.shift_count_diff[worker.id, shift.name] == self.model.shift_count_diff[worker.id, shift.name]
) )
if self.constraint_options["balance_shifts_quadratic"]: if self.constraint_options_model.balance_shifts_quadratic:
# This may need to be updated # This may need to be updated
xU = 25 xU = 25
xL = 1 xL = 1
@@ -2159,7 +2130,7 @@ class RotaBuilder(object):
# NOTE: as this balances across the sum of the worker's shifts # NOTE: as this balances across the sum of the worker's shifts
# they can cancel each other out if a high weighting is given to multiple shifts # they can cancel each other out if a high weighting is given to multiple shifts
if self.constraint_options["balance_shifts_over_workers"]: if self.constraint_options_model.balance_shifts_over_workers:
self.model.constraints.add( self.model.constraints.add(
self.model.worker_shift_count_t1[worker.id] self.model.worker_shift_count_t1[worker.id]
- self.model.worker_shift_count_t2[worker.id] - self.model.worker_shift_count_t2[worker.id]
@@ -2170,7 +2141,7 @@ class RotaBuilder(object):
) )
) )
if self.constraint_options["balance_bank_holidays"]: if self.constraint_options_model.balance_bank_holidays:
extra_bank_holiday = 0 extra_bank_holiday = 0
if self.use_bank_holiday_extra: if self.use_bank_holiday_extra:
extra_bank_holiday = worker.bank_holiday_extra extra_bank_holiday = worker.bank_holiday_extra
@@ -2205,7 +2176,7 @@ class RotaBuilder(object):
>= xU * (self.model.bank_holiday_count[worker.id] + 1) * 2 - xU * xU >= xU * (self.model.bank_holiday_count[worker.id] + 1) * 2 - xU * xU
) )
if self.constraint_options["balance_nights"]: if self.constraint_options_model.balance_nights:
self.model.constraints.add( self.model.constraints.add(
self.model.night_shift_count[worker.id] self.model.night_shift_count[worker.id]
== sum( == sum(
@@ -2280,7 +2251,7 @@ class RotaBuilder(object):
worker.weekend_shift_target_number = weekend_shift_target_number worker.weekend_shift_target_number = weekend_shift_target_number
if self.constraint_options["balance_weekends"]: if self.constraint_options_model.balance_weekends:
if weekend_shift_target_number > 0: if weekend_shift_target_number > 0:
self.model.constraints.add( self.model.constraints.add(
self.model.weekend_shift_count_t1[worker.id] self.model.weekend_shift_count_t1[worker.id]
@@ -2289,7 +2260,7 @@ class RotaBuilder(object):
- weekend_shift_target_number - weekend_shift_target_number
) )
xU = self.constraint_options["max_weekends"] xU = self.constraint_options_model.max_weekends
xL = 1 xL = 1
self.model.constraints.add( self.model.constraints.add(
@@ -2357,17 +2328,15 @@ class RotaBuilder(object):
] ]
) )
if self.constraint_options["balance_blocks"]: if self.constraint_options_model.balance_blocks:
if self.constraint_options["max_night_frequency"]: if self.constraint_options_model.max_night_frequency:
for week_blocks in self.get_week_block_iterator( for week_blocks in self.get_week_block_iterator(
self.constraint_options["max_night_frequency"] self.constraint_options_model.max_night_frequency
): ):
# Ignore excluded weeks # Ignore excluded weeks
if set(week_blocks).intersection( if set(week_blocks).intersection(
set( set(
self.constraint_options[ self.constraint_options_model.max_night_frequency_week_exclusions
"max_night_frequency_week_exclusions"
]
) )
): ):
continue continue
@@ -2387,10 +2356,10 @@ class RotaBuilder(object):
) )
) )
# if self.constraint_options["balance_weekends"]: # if self.constraint_options_model.balance_weekends:
if self.constraint_options["max_weekend_frequency"]: if self.constraint_options_model.max_weekend_frequency:
for week_blocks in self.get_week_block_iterator( for week_blocks in self.get_week_block_iterator(
self.constraint_options["max_weekend_frequency"] self.constraint_options_model.max_weekend_frequency
): ):
# Prevent weekend shifts more than once every n weeks # Prevent weekend shifts more than once every n weeks
self.model.constraints.add( self.model.constraints.add(
@@ -2474,7 +2443,7 @@ class RotaBuilder(object):
try: try:
self.model.constraints.add( self.model.constraints.add(
self.constraint_options["max_shifts_per_week"] self.constraint_options_model.max_shifts_per_week
>= sum( >= sum(
self.model.works[worker.id, w, day, shiftname] self.model.works[worker.id, w, day, shiftname]
# for shiftname in self.get_shift_names_by_week_day(week, day) # for shiftname in self.get_shift_names_by_week_day(week, day)
@@ -2490,7 +2459,7 @@ class RotaBuilder(object):
# TODO: consider excluding shifts that span relevant period # TODO: consider excluding shifts that span relevant period
if ( if (
worker.site worker.site
in self.constraint_options["prevent_monday_after_full_weekends"] in self.constraint_options_model.prevent_monday_after_full_weekends
): ):
# Ignore last week # Ignore last week
if week + 1 in self.weeks: if week + 1 in self.weeks:
@@ -2517,9 +2486,7 @@ class RotaBuilder(object):
) )
if ( if (
worker.site worker.site
in self.constraint_options[ in self.constraint_options_model.prevent_monday_and_tuesday_after_full_weekends
"prevent_monday_and_tuesday_after_full_weekends"
]
): ):
# Ignore last week # Ignore last week
if week + 1 in self.weeks: if week + 1 in self.weeks:
@@ -2552,7 +2519,7 @@ class RotaBuilder(object):
) )
if ( if (
worker.site worker.site
in self.constraint_options["prevent_fridays_before_full_weekends"] in self.constraint_options_model.prevent_fridays_before_full_weekends
): ):
self.model.constraints.add( self.model.constraints.add(
full_weekend_count full_weekend_count
@@ -2571,7 +2538,7 @@ class RotaBuilder(object):
) )
if ( if (
worker.site worker.site
in self.constraint_options["prevent_thursdays_before_full_weekends"] in self.constraint_options_model.prevent_thursdays_before_full_weekends
): ):
self.model.constraints.add( self.model.constraints.add(
full_weekend_count full_weekend_count
@@ -2850,7 +2817,7 @@ class RotaBuilder(object):
# IF paired we check the following against both workers # IF paired we check the following against both workers
workers = [worker] workers = [worker]
if self.constraint_options["hard_constrain_pair_separation"]: if self.constraint_options_model.hard_constrain_pair_separation:
for worker_pairs in self.worker_pairs: for worker_pairs in self.worker_pairs:
if worker_pairs[0] == worker: if worker_pairs[0] == worker:
workers = worker_pairs workers = worker_pairs
@@ -2958,7 +2925,7 @@ class RotaBuilder(object):
) )
) )
if self.constraint_options["hard_constrain_pair_separation"]: if self.constraint_options_model.hard_constrain_pair_separation:
for worker_pairs in self.worker_pairs: for worker_pairs in self.worker_pairs:
if worker_pairs[0] == worker: if worker_pairs[0] == worker:
self.model.constraints.add( self.model.constraints.add(
@@ -3130,7 +3097,7 @@ class RotaBuilder(object):
locum_shift_balancing = 0 locum_shift_balancing = 0
if self.get_workers_who_require_locums(): if self.get_workers_who_require_locums():
if self.constraint_options["balance_locum_shifts"]: if self.constraint_options_model.balance_locum_shifts:
locum_shift_balancing = sum( locum_shift_balancing = sum(
locum_shift_balance_modifier_constant locum_shift_balance_modifier_constant
* ( * (
@@ -3140,7 +3107,7 @@ class RotaBuilder(object):
for worker in self.workers for worker in self.workers
) )
if self.constraint_options["balance_shifts_over_workers"]: if self.constraint_options_model.balance_shifts_over_workers:
worker_shift_balancing = sum( worker_shift_balancing = sum(
balance_modifier_constant balance_modifier_constant
* ( * (
@@ -3152,7 +3119,7 @@ class RotaBuilder(object):
else: else:
worker_shift_balancing = 0 worker_shift_balancing = 0
if self.constraint_options["balance_shifts"]: if self.constraint_options_model.balance_shifts:
shift_balancing = sum( shift_balancing = sum(
balance_modifier_constant balance_modifier_constant
* ( * (
@@ -3165,7 +3132,7 @@ class RotaBuilder(object):
else: else:
shift_balancing = 0 shift_balancing = 0
if self.constraint_options["balance_shifts_quadratic"]: if self.constraint_options_model.balance_shifts_quadratic:
quadratic_shift_balancing = sum( quadratic_shift_balancing = sum(
balance_quadratic_shift_modifier_constant balance_quadratic_shift_modifier_constant
* ( * (
@@ -3179,7 +3146,7 @@ class RotaBuilder(object):
quadratic_shift_balancing = 0 quadratic_shift_balancing = 0
true_quadratic_shift_balancing = 0 true_quadratic_shift_balancing = 0
if self.constraint_options["balance_shifts_true_quadratic"]: if self.constraint_options_model.balance_shifts_true_quadratic:
shift_diff_modifier_constant = 10 shift_diff_modifier_constant = 10
true_quadratic_shift_balancing = sum( true_quadratic_shift_balancing = sum(
shift_diff_modifier_constant shift_diff_modifier_constant
@@ -3187,7 +3154,7 @@ class RotaBuilder(object):
for worker in self.workers for worker in self.workers
) )
if self.constraint_options["minimise_shift_diffs"]: if self.constraint_options_model.minimise_shift_diffs:
shift_diff_modifier_constant = 10 shift_diff_modifier_constant = 10
shift_diff_balancing = sum( shift_diff_balancing = sum(
shift_diff_modifier_constant shift_diff_modifier_constant
@@ -3197,7 +3164,7 @@ class RotaBuilder(object):
else: else:
shift_diff_balancing = 0 shift_diff_balancing = 0
if self.constraint_options["balance_nights"]: if self.constraint_options_model.balance_nights:
night_balance_modifier_constant = 10 night_balance_modifier_constant = 10
night_shift_balancing = sum( night_shift_balancing = sum(
night_balance_modifier_constant night_balance_modifier_constant
@@ -3209,7 +3176,7 @@ class RotaBuilder(object):
else: else:
night_shift_balancing = 0 night_shift_balancing = 0
if self.constraint_options["balance_bank_holidays"]: if self.constraint_options_model.balance_bank_holidays:
bank_holiday_balance_modifier_constant = 1000 bank_holiday_balance_modifier_constant = 1000
bank_holiday_balancing = sum( bank_holiday_balancing = sum(
bank_holiday_balance_modifier_constant bank_holiday_balance_modifier_constant
@@ -3221,7 +3188,7 @@ class RotaBuilder(object):
else: else:
bank_holiday_balancing = 0 bank_holiday_balancing = 0
if self.constraint_options["balance_weekends"]: if self.constraint_options_model.balance_weekends:
weekend_balance_modifier_constant = 5 weekend_balance_modifier_constant = 5
weekend_shift_balancing = sum( weekend_shift_balancing = sum(
weekend_balance_modifier_constant weekend_balance_modifier_constant
@@ -3252,7 +3219,7 @@ class RotaBuilder(object):
) )
# # Spread nights # # Spread nights
if self.constraint_options["balance_nights_across_sites"]: if self.constraint_options_model.balance_nights_across_sites:
nights_site_balancing = sum( nights_site_balancing = sum(
( (
self.model.night_per_site_t1[week, shift.name, site] self.model.night_per_site_t1[week, shift.name, site]
@@ -3282,7 +3249,7 @@ class RotaBuilder(object):
else: else:
block_site_balancing = 0 block_site_balancing = 0
if self.constraint_options["balance_blocks"]: if self.constraint_options_model.balance_blocks:
blocks_balancing = sum( blocks_balancing = sum(
block_shift_balancing_constant block_shift_balancing_constant
* self.model.blocks_assigned[week, shift] * self.model.blocks_assigned[week, shift]
@@ -3446,14 +3413,14 @@ class RotaBuilder(object):
def add_worker_name_constraint_by_week( def add_worker_name_constraint_by_week(
self, names: Iterable[str], weeks: Iterable[int], shifts self, names: Iterable[str], weeks: Iterable[int], shifts
): ):
self.constraint_options["avoid_shifts_by_worker_names"].append( self.constraint_options_model.avoid_shifts_by_worker_names.append(
{"names": names, "weeks": weeks, "shifts": shifts} {"names": names, "weeks": weeks, "shifts": shifts}
) )
def add_grade_constraint_by_week( def add_grade_constraint_by_week(
self, grades: Iterable[int], weeks: Iterable[int], shifts self, grades: Iterable[int], weeks: Iterable[int], shifts
): ):
self.constraint_options["avoid_shifts_by_grades"].append( self.constraint_options_model.avoid_shifts_by_grades.append(
{"grades": grades, "weeks": weeks, "shifts": shifts} {"grades": grades, "weeks": weeks, "shifts": shifts}
) )
@@ -3473,7 +3440,7 @@ class RotaBuilder(object):
weeks=weeks, weeks=weeks,
days=days, days=days,
) )
self.constraint_options["min_summed_grade_by_shifts_per_day"].append(constraint) self.constraint_options_model.min_summed_grade_by_shifts_per_day.append(constraint)
def add_shift(self, shift: SingleShift) -> None: def add_shift(self, shift: SingleShift) -> None:
"""Add a shift to the collection """Add a shift to the collection
@@ -4558,7 +4525,7 @@ class RotaBuilder(object):
+ f"#weekends_worked: {model.worker_weekend_count[worker.id].value}\\#" + f"#weekends_worked: {model.worker_weekend_count[worker.id].value}\\#"
) )
if self.constraint_options["balance_bank_holidays"]: if self.constraint_options_model.balance_bank_holidays:
bank_holiday_count = model.bank_holiday_count[worker.id].value bank_holiday_count = model.bank_holiday_count[worker.id].value
bank_holiday_count_w = model.bank_holiday_count_w[worker.id].value - 1 bank_holiday_count_w = model.bank_holiday_count_w[worker.id].value - 1