File size: 6,488 Bytes
210d88d
 
 
 
a935bbc
210d88d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a935bbc
210d88d
 
d9286d1
 
210d88d
a935bbc
 
210d88d
 
 
 
 
 
d9286d1
 
210d88d
 
 
 
 
 
 
 
 
fb92003
210d88d
5198597
210d88d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# solver.py

from typing import List, Dict, Optional, Any, Tuple
from ortools.sat.python import cp_model
import os

from models import Task, Faculty, Section, Room
from constraint_engine import ConstraintEngine

class TimetableSolver:

    def __init__(self, tasks, faculties, sections, rooms):
        self.tasks     = tasks
        self.faculties = faculties
        self.sections  = sections
        self.rooms     = rooms
        self.model     = cp_model.CpModel()
        self.solver    = cp_model.CpSolver()
        self.constraint_engine = None
        self.objective_engine  = None

    def solve(
        self,
        time_limit_seconds: int = 60,
        enable_soft_constraints: bool = True,
        soft_constraint_weights: Dict[str, int] = None,
        log_search_progress: bool = True,
        num_workers: int = None,
        slm_constraints: List[Dict[str, Any]] = None,
        scheduling_rules: List[Dict[str, Any]] = None,
        locked_schedule: Dict[str, Any] = None,
        locked_semesters: List[int] = None,
    ) -> Tuple[str, Optional[Dict[str, Any]]]:
        if num_workers is None:
            num_workers = os.cpu_count() or 8

        # 1. Hard constraints
        print("Initializing Constraint Engine...")
        self.constraint_engine = ConstraintEngine(
            model=self.model, tasks=self.tasks, faculties=self.faculties,
            sections=self.sections, rooms=self.rooms,
            slm_constraints=slm_constraints, scheduling_rules=scheduling_rules,
            locked_schedule=locked_schedule, locked_semesters=locked_semesters
        )
        self.constraint_engine.apply_all_constraints()

        # 2. Soft constraints
        if enable_soft_constraints:
            print("Initializing Objective Engine...")
            from objective_engine import ObjectiveEngine
            self.objective_engine = ObjectiveEngine(
                model=self.model,
                ce=self.constraint_engine,
                tasks=self.tasks, faculties=self.faculties,
                sections=self.sections,
                weights=soft_constraint_weights
            )
            self.objective_engine.build_objective()

        # 4. Configure solver
        self.solver.parameters.max_time_in_seconds  = time_limit_seconds
        self.solver.parameters.log_search_progress  = log_search_progress
        self.solver.parameters.num_search_workers   = num_workers

        # 5. Solve
        print(f"Starting solver (Limit: {time_limit_seconds}s)...")
        status_val = self.solver.Solve(self.model)

        status_map = {
            cp_model.OPTIMAL:       "OPTIMAL",
            cp_model.FEASIBLE:      "FEASIBLE",
            cp_model.INFEASIBLE:    "INFEASIBLE",
            cp_model.MODEL_INVALID: "MODEL_INVALID",
            cp_model.UNKNOWN:       "UNKNOWN"
        }
        status_str = status_map.get(status_val, "UNKNOWN")
        print(f"Solver finished: {status_str}")

        solution = None
        if status_val in (cp_model.OPTIMAL, cp_model.FEASIBLE):
            solution = self._extract_solution()
            print(f"Solution found! Objective: {self.solver.ObjectiveValue()}")
        else:
            print("No solution found.")

        return status_str, solution

    def _extract_solution(self):
        import constants as const
        schedule = {}
        for task in self.tasks:
            start_var, _, _, room_var = self.constraint_engine.task_vars[task.task_id]
            start_slot   = self.solver.Value(start_var)
            room_index   = self.solver.Value(room_var)
            assigned_room = self.rooms[room_index]
            day_index    = start_slot // const.NUM_TEACHING_SLOTS_PER_DAY
            period_index = start_slot  % const.NUM_TEACHING_SLOTS_PER_DAY
            schedule[task.task_id] = {
                "task_obj":     task,
                "start_slot":   start_slot,
                "day_index":    day_index,
                "day_name":     const.DAYS[day_index],
                "period_index": period_index,
                "room_id":      assigned_room.room_id,
                "room_name":    f"{assigned_room.room_id} ({assigned_room.building})",
                "faculty_name": task.faculty.name,
                "subject_code": task.subject.subject_code,
                "section_id":   task.section.section_id,
                "duration":     task.duration
            }
        return schedule

    def diagnose_infeasibility(self) -> str:
        """Heuristic checks to explain common reasons for INFEASIBLE models."""
        import constants as const
        # 1. Check Faculty Overload
        fac_hours = {}
        for t in self.tasks:
            if 'DUMMY' in t.faculty.id: continue
            for fid in t.faculty.id.split('_'):
                fac_hours[fid] = fac_hours.get(fid, 0) + t.duration
        
        for fid, hrs in fac_hours.items():
            if hrs > const.TOTAL_TEACHING_SLOTS_PER_WEEK:
                fac = next((f.name for f in self.faculties if f.id == fid), fid)
                return f"Faculty overload: {fac} ({fid}) is assigned {hrs} hours of classes, but there are only {const.TOTAL_TEACHING_SLOTS_PER_WEEK} total slots available in the week."
        
        # 2. Check Section Overload
        sec_parent_hours = {}
        sec_batch_hours = {}
        for t in self.tasks:
            sec = t.section.section_id
            if '-' in sec:
                parent = sec.split('-')[0]
                if parent not in sec_batch_hours:
                    sec_batch_hours[parent] = {}
                sec_batch_hours[parent][sec] = sec_batch_hours[parent].get(sec, 0) + t.duration
            else:
                sec_parent_hours[sec] = sec_parent_hours.get(sec, 0) + t.duration
                
        for sid, hrs in sec_parent_hours.items():
            max_batch = 0
            if sid in sec_batch_hours:
                max_batch = max(sec_batch_hours[sid].values())
            total = hrs + max_batch
            if total > const.TOTAL_TEACHING_SLOTS_PER_WEEK:
                return f"Section overload: Section {sid} (including its batches) requires {total} hours of classes, but there are only {const.TOTAL_TEACHING_SLOTS_PER_WEEK} slots available in the week."

        return "The constraint engine could not find a solution due to conflicting constraints. This typically happens if multiple subjects are forced to occur at the same time, or if teachers/rooms are double-booked by rigid scheduling rules."