File size: 10,500 Bytes
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# data_loader.py

"""

This module is responsible for converting raw input data into a list of

atomic 'Task' objects that the solver can schedule.



Responsibilities:

- Ingest lists of faculties, subjects, sections, and their allocations.

- Convert subject credits into the correct number of weekly Task instances.

- Ensure THEORY subjects create 1-hour tasks per credit.

- Ensure LAB, SOFTSKILL, and FORUM subjects create a single 2-hour task.

- Assign a unique ID to each task.

- Populate each task with its corresponding faculty, subject, and section.

- Correctly handle elective groups for multi-hour subjects.



This module performs NO scheduling logic, constraint creation, or optimization.

It is purely a data transformation and preparation layer.

"""

from dataclasses import dataclass
from typing import List, Optional

# Import the core data models
from models import Faculty, Subject, Section, Task, SubjectType
from constants import TOTAL_TEACHING_SLOTS_PER_WEEK

@dataclass(frozen=True)
class Allocation:
    """

    A simple dataclass to represent the raw input mapping of who teaches what

    to whom. This is a cleaner alternative to using tuples or dicts.

    """
    faculty_id: str
    subject_code: str
    section_id: str
    elective_group_id: Optional[str] = None


def prepare_scheduling_tasks(

    allocations: List[Allocation],

    faculties: List[Faculty],

    subjects: List[Subject],

    sections: List[Section]

) -> List[Task]:
    """

    Processes raw allocation data and generates a flat list of atomic Task

    objects ready for the solver.



    Args:

        allocations: A list of Allocation objects defining the teaching load.

        faculties: A list of all available Faculty objects.

        subjects: A list of all available Subject objects.

        sections: A list of all available Section objects.



    Returns:

        A list of Task objects, where each task is an atomic unit to be scheduled.

    """
    # Create lookup dictionaries for efficient access
    faculties_by_id = {f.id: f for f in faculties}
    subjects_by_code = {s.subject_code: s for s in subjects}
    sections_by_id = {s.section_id: s for s in sections}

    all_tasks: List[Task] = []
    section_duration_sums = {s.section_id: 0 for s in sections}

    # Group allocations for co-teaching support
    from collections import defaultdict
    grouped_allocs = defaultdict(list)
    for alloc in allocations:
        key = (alloc.subject_code, alloc.section_id, alloc.elective_group_id)
        grouped_allocs[key].append(alloc)

    for key, alloc_group in grouped_allocs.items():
        subject_code, section_id, alloc_elective_group_id = key
        
        try:
            subject = subjects_by_code[subject_code]
            section = sections_by_id[section_id]
            facs = [faculties_by_id[a.faculty_id] for a in alloc_group]
        except KeyError as e:
            print(f"Error: Invalid ID in allocation group {key}. Missing key: {e}")
            continue
            
        if len(facs) == 1:
            faculty = facs[0]
        else:
            # Create composite faculty to avoid generating duplicate tasks
            comp_id = "_".join(f.id for f in facs)
            comp_name = " / ".join(f.name.replace("Prof. ", "").replace("Dr. ", "").replace("Mr. ", "").replace("Ms. ", "").strip() for f in facs)
            faculty = Faculty(id=comp_id, name=comp_name, designation="Co-Teaching", max_hours_per_week=99)

        # --- Task Generation Logic ---

        if subject.subject_type == SubjectType.THEORY:
            # For a theory subject, create one 1-hour task for each credit.
            for i in range(subject.credits):
                # *** CRITICAL FIX FOR ELECTIVES ***
                # For a 3-credit elective, we need groups like 'group_0', 'group_1', 'group_2'
                # to pair the correct hours across different sections.
                
                group_id = None
                if alloc_elective_group_id:
                    group_id = f"{alloc_elective_group_id}_{i}"
                elif subject.is_core:
                    # Treat Core Theory as a "Joint Class" for all batches in the section (e.g. 6a-E1 + 6a-E2)
                    # We generate a synthetic group ID: CORE_CS101_6A_0
                    parent_sec = section.section_id.split('-')[0]
                    group_id = f"CORE_{subject.subject_code}_{parent_sec}_{i}"

                task = Task(
                    task_id=f"{subject.subject_code}-{section.section_id}-{i}",
                    faculty=faculty,
                    subject=subject,
                    section=section,
                    duration=1,
                    elective_group_id=group_id
                )
                all_tasks.append(task)
                section_duration_sums[section.section_id] += task.duration
        
        elif subject.subject_type in [SubjectType.LAB, SubjectType.SOFTSKILL, SubjectType.FORUM]:
            # For labs and other block sessions, create exactly ONE 2-hour task.
            # The elective group applies to the entire 2-hour block as a single unit.
            task = Task(
                task_id=f"{subject.subject_code}-{section.section_id}-BLOCK",
                faculty=faculty,
                subject=subject,
                section=section,
                duration=2,
                elective_group_id=alloc_elective_group_id
            )
            all_tasks.append(task)
            section_duration_sums[section.section_id] += task.duration
    # --- Gap Filler Logic ---
    try:
        if "DUMMY_STAFF" in faculties_by_id and "LIB_HR" in subjects_by_code:
            dummy_fac = faculties_by_id["DUMMY_STAFF"]
            dummy_subjects_pool = [
                subjects_by_code["LIB_HR"],
                subjects_by_code["STU_HR"],
                subjects_by_code["FAC_HR"],
                subjects_by_code["STDY_HR"]
            ]
            
            for section in sections:
                if '-' in section.section_id or section.section_id == 'OE_AI':
                    continue
                batch_durations = [
                    section_duration_sums.get(b.section_id, 0)
                    for b in sections if b.section_id.startswith(f"{section.section_id}-")
                ]
                max_batch_duration = max(batch_durations) if batch_durations else 0
                current_duration = section_duration_sums.get(section.section_id, 0) + max_batch_duration
                
                # Dynamically adjust filler buffer based on problem size (massive math = lower fillers)
                num_allocations = len(allocations)
                if num_allocations > 100:
                    buffer = 8  # Massive math: keep 8 hours free to reduce dummy tasks
                elif num_allocations > 50:
                    buffer = 4  # Moderate math: keep 4 hours free
                else:
                    buffer = 2  # Normal math: keep 2 hours free
                    
                gaps = max(0, TOTAL_TEACHING_SLOTS_PER_WEEK - current_duration - buffer)
                
                if gaps > 0:
                    for i in range(gaps):
                        subj = dummy_subjects_pool[i % len(dummy_subjects_pool)]
                        task = Task(
                            task_id=f"FILLER-{subj.subject_code}-{section.section_id}-{i}",
                            faculty=dummy_fac,
                            subject=subj,
                            section=section,
                            duration=1,
                            elective_group_id=None
                        )
                        all_tasks.append(task)
                        # No need to update duration_sums since we are done
    except KeyError as e:
        print(f"Skipping gap filling, missing dummy setup: {e}")

    return all_tasks


# --- Example Usage ---
if __name__ == '__main__':
    # This block demonstrates how to use the prepare_scheduling_tasks function.
    # It will only run when this file is executed directly.

    # 1. Define sample data using the core models
    faculty1 = Faculty(id="F001", name="Dr. Smith", designation="Professor", max_hours_per_week=10)
    faculty2 = Faculty(id="F002", name="Dr. Jones", designation="Asst. Professor", max_hours_per_week=12)

    subject_theory = Subject(subject_code="CS101", name="Intro to CS", credits=4, subject_type=SubjectType.THEORY)
    subject_lab = Subject(subject_code="CS101L", name="CS Lab", credits=1, subject_type=SubjectType.LAB)
    subject_elective = Subject(subject_code="CS555", name="Advanced AI", credits=3, subject_type=SubjectType.THEORY)


    section_a = Section(section_id="5A", semester=5, student_strength=60)
    section_b = Section(section_id="5B", semester=5, student_strength=62)

    # 2. Define the teaching allocations
    sample_allocations = [
        Allocation(faculty_id="F001", subject_code="CS101", section_id="5A"),
        Allocation(faculty_id="F002", subject_code="CS101L", section_id="5A"),
        # Elective subject taught by the same faculty to both sections
        Allocation(faculty_id="F001", subject_code="CS555", section_id="5A", elective_group_id="ELEC01"),
        Allocation(faculty_id="F001", subject_code="CS555", section_id="5B", elective_group_id="ELEC01"),
    ]

    # 3. Call the data loader function
    generated_tasks = prepare_scheduling_tasks(
        allocations=sample_allocations,
        faculties=[faculty1, faculty2],
        subjects=[subject_theory, subject_lab, subject_elective],
        sections=[section_a, section_b]
    )

    # 4. Print the results to verify
    print(f"--- Generated {len(generated_tasks)} Tasks ---\n")
    for task in generated_tasks:
        print(f"Task ID: {task.task_id}")
        print(f"  Subject: {task.subject.name} ({task.subject.subject_type.name})")
        print(f"  Faculty: {task.faculty.name}")
        print(f"  Section: {task.section.section_id}")
        print(f"  Duration: {task.duration} hour(s)")
        if task.elective_group_id:
            print(f"  Elective Group: {task.elective_group_id}")
        print("-" * 20)

    # Expected Output:
    # Elective tasks for CS555 will now have group IDs like "ELEC01_0", "ELEC01_1", "ELEC01_2"