Spaces:
Sleeping
Sleeping
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" |