Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 55 additions & 62 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,71 +1,64 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
.env
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
!frontend/src/lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
venv/
ENV/
migrations/
```
# Dependencies
frontend/node_modules/

# Flask
instance/
.webassets-cache
.env
# Build artifacts
frontend/node_modules/**/dist/
frontend/node_modules/**/build/
frontend/node_modules/**/target/

# Vite & Frontend
dist/
dist-ssr/
.vite/
*.local
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.eslintcache
# Package manager specific
package-lock.json
yarn.lock

# Editor & OS
.vscode/*
!.vscode/extensions.json
# IDE files
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.swp
*.swo

# OS files
.DS_Store
Thumbs.db

# Agent AI
.gemini/
.agents/
.agent/
_agents/
_agent/
# Logs
*.log
tmp/
*.tmp
.pytest_cache/
pytest-cache-files-*/
backend/pytest-cache-files-*/
backend/.pytest_cache/

# Environment
.env
.env.local
*.env.*

# Coverage
coverage/
htmlcov/
.coverage

# TypeScript
*.tsbuildinfo

# Compressed files
*.zip
*.gz
*.tar
*.tgz
*.bz2
*.xz
*.7z
*.rar
*.zst
*.lz4
*.lzh
*.cab
*.arj
*.rpm
*.deb
*.Z
*.lz
*.lzo
*.tar.gz
*.tar.bz2
*.tar.xz
*.tar.zst

```
2 changes: 1 addition & 1 deletion backend/app/routes/scheduling.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ def gap_penalty(section_id, day, slot):

# Find consecutive slots for the needed duration
for i in range(len(TIMESLOTS) - needed + 1):
block_slots = TIMESLOTS[i:i + needed]
block_slots = TIMESLOTS[i : i + needed]

# Check teacher availability
if any(not is_teacher_available(teacher, day, s) for s in block_slots):
Expand Down
38 changes: 21 additions & 17 deletions backend/app/routes/workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,21 +322,25 @@ def get_workload_summary():
# Calculate total hours (L+T+P)
total_hours = sum(a.course.get_hours_needed() for a in allocations if a.course)

summary.append({
"teacher_id": t.id,
"teacher_name": t.name,
"teacher_email": t.email,
"course_count": course_count,
"total_hours": total_hours,
"departments": [d.name for d in t.departments],
"assignments": [
{
"id": a.id,
"course_code": a.course.code,
"course_name": a.course.name,
"section_name": a.section.name,
"batch_name": a.section.batch.name if a.section.batch else "Unknown"
} for a in allocations if a.course and a.section
]
})
summary.append(
{
"teacher_id": t.id,
"teacher_name": t.name,
"teacher_email": t.email,
"course_count": course_count,
"total_hours": total_hours,
"departments": [d.name for d in t.departments],
"assignments": [
{
"id": a.id,
"course_code": a.course.code,
"course_name": a.course.name,
"section_name": a.section.name,
"batch_name": a.section.batch.name if a.section.batch else "Unknown",
}
for a in allocations
if a.course and a.section
],
}
)
return jsonify(summary), 200
20 changes: 10 additions & 10 deletions backend/app/scheduler_new/data_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,7 @@ def load_sections(
query = Section.query.options(joinedload(Section.batch).joinedload(Batch.program))

if self.department_id:
query = query.join(Section.batch).join(Batch.program).filter(
Program.department_id == self.department_id
)
query = query.join(Section.batch).join(Batch.program).filter(Program.department_id == self.department_id)

sections = self._deduplicate_sections(query.all())
if not sections:
Expand Down Expand Up @@ -432,13 +430,15 @@ def load_problem(self) -> "SchedulingProblem":
for c in all_courses:
if c.id not in active_course_ids:
if (c.program_id, self._resolve_course_semester(c)) in active_section_semesters:
unassigned_curriculum.append({
"id": c.id,
"code": c.code,
"name": c.name,
"type": c.course_type,
"reason": "Missing from Workload Page"
})
unassigned_curriculum.append(
{
"id": c.id,
"code": c.code,
"name": c.name,
"type": c.course_type,
"reason": "Missing from Workload Page",
}
)

return SchedulingProblem(
sections=sections,
Expand Down
60 changes: 36 additions & 24 deletions backend/app/scheduler_new/genetic_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def _precompute_domains(self):
try:
idx = day_slots.index(t)
if idx + hours <= len(day_slots):
consecutive = day_slots[idx:idx + hours]
consecutive = day_slots[idx : idx + hours]
self.slot_keys_cache[(t, hours)] = [(s.day, s.start_time) for s in consecutive]
else:
self.slot_keys_cache[(t, hours)] = None
Expand Down Expand Up @@ -160,18 +160,20 @@ def _get_random_domain_sample(self, var, max_samples=10, forced_teacher=None) ->

samples = []
for _ in range(max_samples):
opt = random.choice(domain)
opt = random.choice(domain) # nosec B311

if forced_teacher is not None:
# Only use this option if it contains the forced teacher
if forced_teacher not in opt["faculties"]:
continue
faculty_id = forced_teacher
else:
faculty_id = random.choice(opt["faculties"])
faculty_id = random.choice(opt["faculties"]) # nosec B311

samples.append(
DomainValue(faculty_id=faculty_id, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"])
DomainValue(
faculty_id=faculty_id, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"]
) # nosec B311
)

return samples
Expand Down Expand Up @@ -328,7 +330,9 @@ def _enforce_consistency_in_individual(
if assigned_teacher in opt["faculties"]:
# Found a matching option - update to use correct teacher
fixed[i] = DomainValue(
faculty_id=assigned_teacher, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"]
faculty_id=assigned_teacher,
room_id=random.choice(opt["rooms"]),
timeslot=opt["timeslot"], # nosec B311
)
found = True
break
Expand Down Expand Up @@ -357,22 +361,26 @@ def _generate_random_individual(self) -> List[Optional[DomainValue]]:
# Use the already-assigned teacher for this (section, course)
valid_opts = [opt for opt in domain if assigned_teacher in opt["faculties"]]
if valid_opts:
opt = random.choice(valid_opts)
opt = random.choice(valid_opts) # nosec B311
individual.append(
DomainValue(
faculty_id=assigned_teacher, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"]
faculty_id=assigned_teacher,
room_id=random.choice(opt["rooms"]),
timeslot=opt["timeslot"], # nosec B311
)
)
else:
# Cannot use assigned teacher - mark as None
individual.append(None)
else:
# First occurrence - randomly select and record teacher
opt = random.choice(domain)
teacher = random.choice(opt["faculties"])
opt = random.choice(domain) # nosec B311
teacher = random.choice(opt["faculties"]) # nosec B311
section_course_faculty[sc_key] = teacher
individual.append(
DomainValue(faculty_id=teacher, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"])
DomainValue(
faculty_id=teacher, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"]
) # nosec B311
)

return individual
Expand All @@ -383,7 +391,7 @@ def _crossover_single_point(
"""Single-point crossover"""
if len(self.variables) < 2:
return list(parent1), list(parent2)
point = random.randint(1, len(self.variables) - 1)
point = random.randint(1, len(self.variables) - 1) # nosec B311
child1 = parent1[:point] + parent2[point:]
child2 = parent2[:point] + parent1[point:]
return child1, child2
Expand All @@ -394,8 +402,8 @@ def _crossover_two_point(
"""Two-point crossover"""
if len(self.variables) < 3:
return self._crossover_single_point(parent1, parent2)
point1 = random.randint(1, len(self.variables) - 2)
point2 = random.randint(point1 + 1, len(self.variables) - 1)
point1 = random.randint(1, len(self.variables) - 2) # nosec B311
point2 = random.randint(point1 + 1, len(self.variables) - 1) # nosec B311
child1 = parent1[:point1] + parent2[point1:point2] + parent1[point2:]
child2 = parent2[:point1] + parent1[point1:point2] + parent2[point2:]
return child1, child2
Expand All @@ -407,7 +415,7 @@ def _crossover_uniform(
child1 = []
child2 = []
for i in range(len(self.variables)):
if random.random() < 0.5:
if random.random() < 0.5: # nosec B311
child1.append(parent1[i])
child2.append(parent2[i])
else:
Expand All @@ -419,7 +427,7 @@ def _crossover(
self, parent1: List[Optional[DomainValue]], parent2: List[Optional[DomainValue]]
) -> Tuple[List[Optional[DomainValue]], List[Optional[DomainValue]]]:
"""Adaptive crossover: randomly select operator"""
operator = random.choice(["single", "two_point", "uniform"])
operator = random.choice(["single", "two_point", "uniform"]) # nosec B311
if operator == "single":
return self._crossover_single_point(parent1, parent2)
elif operator == "two_point":
Expand All @@ -441,19 +449,19 @@ def _mutate(self, individual: List[Optional[DomainValue]]) -> List[Optional[Doma
section_course_faculty[sc_key] = val.faculty_id

for i, var in enumerate(self.variables):
if random.random() < self.current_mutation_rate:
if random.random() < self.current_mutation_rate: # nosec B311
sc_key = (var.section_id, var.course_id)
assigned_teacher = section_course_faculty.get(sc_key)

sampled = self._get_random_domain_sample(var, 5, forced_teacher=assigned_teacher)
if sampled:
if len(sampled) > 1 and random.random() < 0.5:
if len(sampled) > 1 and random.random() < 0.5: # nosec B311
current_val = mutated[i]
available = [d for d in sampled if d != current_val]
if available:
mutated[i] = random.choice(available)
mutated[i] = random.choice(available) # nosec B311
else:
mutated[i] = random.choice(sampled)
mutated[i] = random.choice(sampled) # nosec B311

return mutated

Expand Down Expand Up @@ -721,15 +729,19 @@ def solve(self, progress_callback: Optional[Callable[[int, str], None]] = None)
for i in range(self.elite_size):
elite = fitness_scores[i][1]
# Apply local search to elite individuals
if self.use_local_search and random.random() < 0.3:
if self.use_local_search and random.random() < 0.3: # nosec B311
elite = self._local_search_hill_climbing(elite, self.local_search_intensity // 2)
new_population.append(elite)

# Generate rest of new population
while len(new_population) < self.population_size:
# Tournament selection
tournament1 = random.sample(fitness_scores, min(self.tournament_size, len(fitness_scores)))
tournament2 = random.sample(fitness_scores, min(self.tournament_size, len(fitness_scores)))
tournament1 = random.sample(
fitness_scores, min(self.tournament_size, len(fitness_scores))
) # nosec B311
tournament2 = random.sample(
fitness_scores, min(self.tournament_size, len(fitness_scores))
) # nosec B311

parent1 = min(tournament1, key=lambda x: x[0])[1]
parent2 = min(tournament2, key=lambda x: x[0])[1]
Expand All @@ -740,9 +752,9 @@ def solve(self, progress_callback: Optional[Callable[[int, str], None]] = None)
child2 = self._mutate(child2)

# Apply repair to offspring
if random.random() < 0.2:
if random.random() < 0.2: # nosec B311
child1 = self._repair_individual(child1)
if random.random() < 0.2:
if random.random() < 0.2: # nosec B311
child2 = self._repair_individual(child2)

new_population.append(child1)
Expand Down
2 changes: 1 addition & 1 deletion backend/app/scheduler_new/greedy_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def solve(self, progress_callback: Optional[Callable[[int, str], None]] = None)
for h in range(1, 8):
for i in range(num_timeslots):
if i + h <= num_timeslots:
consecutive_slots = timeslot_list[i:i + h]
consecutive_slots = timeslot_list[i : i + h]
valid = True
for j in range(h - 1):
if (
Expand Down
Loading
Loading