@@ -123,11 +123,15 @@ def _initialize_faculty_data(self, config) -> None:
123123 faculty_name = faculty_data .name
124124 self ._faculty .add (faculty_name )
125125 self ._faculty_maximum_credits [faculty_name ] = faculty_data .maximum_credits
126+ self ._faculty_maximum_days [faculty_name ] = faculty_data .maximum_days
126127 self ._faculty_minimum_credits [faculty_name ] = faculty_data .minimum_credits
127128 self ._faculty_unique_course_limits [faculty_name ] = faculty_data .unique_course_limit
128129 self ._faculty_course_preferences [faculty_name ] = faculty_data .course_preferences
129130 self ._faculty_room_preferences [faculty_name ] = faculty_data .room_preferences
130131 self ._faculty_lab_preferences [faculty_name ] = faculty_data .lab_preferences
132+ self ._faculty_mandatory_days [faculty_name ] = {
133+ day if isinstance (day , Day ) else Day [day ] for day in faculty_data .mandatory_days
134+ }
131135 self ._faculty_availability [faculty_name ] = get_faculty_availability (faculty_data )
132136
133137 def _initialize_courses (self , config ) -> tuple [list [Course ], set [int ]]:
@@ -246,11 +250,13 @@ def __init__(self, full_config: CombinedConfig):
246250 # Initialize data structures
247251 self ._faculty : set [str ] = set ()
248252 self ._faculty_maximum_credits : dict [str , int ] = dict ()
253+ self ._faculty_maximum_days : dict [str , int ] = dict ()
249254 self ._faculty_minimum_credits : dict [str , int ] = dict ()
250255 self ._faculty_unique_course_limits : dict [str , int ] = dict ()
251256 self ._faculty_course_preferences : dict [str , dict [str , int ]] = dict ()
252257 self ._faculty_room_preferences : dict [str , dict [str , int ]] = dict ()
253258 self ._faculty_lab_preferences : dict [str , dict [str , int ]] = dict ()
259+ self ._faculty_mandatory_days : dict [str , set [Day ]] = dict ()
254260 self ._faculty_availability : dict [str , list [TimeInstance ]] = dict ()
255261 self ._initialize_faculty_data (config )
256262
@@ -326,8 +332,8 @@ def _z3ify_time_constraint(
326332 false : list [tuple [z3 .BoolRef , z3 .BoolRef ]] = []
327333
328334 for slot_i , slot_j in itertools .combinations_with_replacement (self ._slots , 2 ):
329- c_i = z3_data .time_slot_constants [slot_i ]
330- c_j = z3_data .time_slot_constants [slot_j ]
335+ c_i = cast ( z3 . BoolRef , z3_data .time_slot_constants [slot_i ])
336+ c_j = cast ( z3 . BoolRef , z3_data .time_slot_constants [slot_j ])
331337 if self ._cached_slot_relationship (name , slot_i , slot_j ):
332338 true .append ((c_i , c_j ))
333339 true .append ((c_j , c_i ))
@@ -361,7 +367,7 @@ def _z3ify_time_slot_fn(
361367 true : list [z3 .BoolRef ] = []
362368 false : list [z3 .BoolRef ] = []
363369 for slot in self ._slots :
364- c = z3_data .time_slot_constants [slot ]
370+ c = cast ( z3 . BoolRef , z3_data .time_slot_constants [slot ])
365371 if fn (slot ):
366372 true .append (c )
367373 else :
@@ -388,9 +394,9 @@ def _z3ify_faculty_time_constraint(
388394 true : list [tuple [z3 .BoolRef , z3 .BoolRef ]] = []
389395 false : list [tuple [z3 .BoolRef , z3 .BoolRef ]] = []
390396 faculty_times = self ._faculty_availability [faculty ]
391- faculty_constant = z3_data .faculty_constants [faculty ]
397+ faculty_constant = cast ( z3 . BoolRef , z3_data .faculty_constants [faculty ])
392398 for slot in self ._slots :
393- slot_constant = z3_data .time_slot_constants [slot ]
399+ slot_constant = cast ( z3 . BoolRef , z3_data .time_slot_constants [slot ])
394400 if slot .in_time_ranges (faculty_times ):
395401 true .append ((faculty_constant , slot_constant ))
396402 else :
@@ -463,11 +469,23 @@ def _build_faculty_constraints(self, z3_data: _Z3SortsAndConstants) -> list[z3.B
463469 for faculty in c .faculties :
464470 faculty_course_map [faculty ].append (c )
465471
472+ # Pre-compute time slot constants per day for reuse
473+ day_slot_map : defaultdict [Day , set [z3 .ExprRef ]] = defaultdict (set )
474+ for slot in self ._slots :
475+ slot_constant = z3_data .time_slot_constants [slot ]
476+ for time_instance in slot .times :
477+ day_slot_map [time_instance .day ].add (slot_constant )
478+ day_to_slot_constants : dict [Day , tuple [z3 .ExprRef , ...]] = {
479+ day : tuple (slot_constants ) for day , slot_constants in day_slot_map .items ()
480+ }
481+
466482 # Add faculty credit and unique course limits - batch generation
467483 faculty_constraints : list [z3 .BoolRef ] = []
468484 for faculty in self ._faculty :
469485 faculty_courses = faculty_course_map .get (faculty , [])
470486 faculty_constant = z3_data .faculty_constants [faculty ]
487+ max_days = self ._faculty_maximum_days [faculty ]
488+ mandatory_days = self ._faculty_mandatory_days [faculty ]
471489 if faculty_courses :
472490 min_credits = self ._faculty_minimum_credits [faculty ]
473491 max_credits = self ._faculty_maximum_credits [faculty ]
@@ -505,6 +523,54 @@ def _build_faculty_constraints(self, z3_data: _Z3SortsAndConstants) -> list[z3.B
505523 )
506524 faculty_constraints .append (limit )
507525
526+ # Track whether the faculty teaches on a given day
527+ day_indicator_map : dict [Day , z3 .BoolRef ] = {}
528+ for day in Day :
529+ slot_constants = day_to_slot_constants .get (day , ())
530+ course_day_assignments : list [z3 .BoolRef ] = []
531+ if slot_constants and faculty_courses :
532+ for course in faculty_courses :
533+ slot_matches = [course .time == slot_const for slot_const in slot_constants ]
534+ if slot_matches :
535+ course_day_assignments .append (
536+ cast (
537+ z3 .BoolRef ,
538+ self ._simplify (
539+ z3 .And (
540+ course .faculty == faculty_constant ,
541+ z3 .Or (slot_matches ),
542+ )
543+ ),
544+ )
545+ )
546+ if course_day_assignments :
547+ day_indicator_map [day ] = cast (
548+ z3 .BoolRef ,
549+ self ._simplify (z3 .Or (course_day_assignments )),
550+ )
551+ else :
552+ day_indicator_map [day ] = z3 .BoolVal (False , ctx = self ._ctx )
553+
554+ # Maximum-day constraint
555+ day_sum_terms = [z3 .If (indicator , 1 , 0 ) for indicator in day_indicator_map .values ()]
556+ day_sum = z3 .Sum (day_sum_terms ) if day_sum_terms else z3 .IntVal (0 , ctx = self ._ctx )
557+ faculty_constraints .append (
558+ cast (
559+ z3 .BoolRef ,
560+ self ._simplify (day_sum <= max_days ),
561+ )
562+ )
563+
564+ # Mandatory-day constraints
565+ for mandatory_day in mandatory_days :
566+ indicator = day_indicator_map .get (mandatory_day , z3 .BoolVal (False , ctx = self ._ctx ))
567+ faculty_constraints .append (
568+ cast (
569+ z3 .BoolRef ,
570+ self ._simplify (indicator ),
571+ )
572+ )
573+
508574 return faculty_constraints
509575
510576 def _build_course_constraints (
@@ -659,6 +725,10 @@ def _build_resource_constraints(
659725 diff_course_constraints .append (z3 .Not (lab_next_to (i .time , j .time )))
660726 constraint_parts .append (cast (z3 .BoolRef , z3 .And (diff_course_constraints )))
661727
728+ if i .course_id == j .course_id :
729+ # prevent overlapping times for different sections of the same course
730+ resource_constraints .append (cast (z3 .BoolRef , z3 .Not (overlaps (i .time , j .time ))))
731+
662732 if resource :
663733 # add all resource constraints (room, lab, etc.)
664734 resource_constraints .append (cast (z3 .BoolRef , z3 .And (resource )))
0 commit comments