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