Skip to content
Merged
2 changes: 0 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,7 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/


# docs of local picture
docs
27 changes: 27 additions & 0 deletions syncortexGA/constraints/hard_constraints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from syncortexGA.models.timetable_model import Timetable


def check_h1_instructor_conflicts(timetable: Timetable) -> bool:
"""
Ensures no instructor is scheduled for more than one session at the same time
and only in their available time slots.
"""
seen = {} # key: (instructor_id, day, start, end), value: count

for session in timetable.sessions:
instructor = session.instructor_id
slot = session.slot

# 1. Check if the session is in instructor's available slots
if slot not in instructor.available_slots:
return False

# 2. Check if this slot already assigned to this instructor
key = (instructor.id, slot.day, slot.start, slot.end)
if key in seen:
return (
False # Conflict detected: instructor already has session in this slot
)
seen[key] = True

return True
146 changes: 84 additions & 62 deletions syncortexGA/models/timetable_model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel, model_validator, StringConstraints
from pydantic import BaseModel, model_validator, StringConstraints, ConfigDict
from typing import List, Optional, Literal, Annotated

# Accepts only time strings in 24-hour format like "08:00", "23:59"
Expand Down Expand Up @@ -39,15 +39,31 @@ class AlternatingSessionPattern(BaseModel):
paired_course_id: int


class LabSessionPattern(BaseModel):
"""
Represents a lab session pattern that occurs weekly at a fixed time.
This is used for lab courses that do not have a session pattern.
"""

slot: TimeSlot

@model_validator(mode="after")
def check_single_slot(cls, model):
if not model.slot:
raise ValueError("LabSessionPattern must have exactly 1 slot")
return model


class SessionPattern(BaseModel):
"""
Wraps either a fixed or alternating session pattern.
Only one of the patterns must be set based on the type.
"""

type: Literal["fixed", "alternating"]
type: Literal["fixed", "alternating", "lab"]
fixed_pattern: Optional[FixedSessionPattern] = None
alternating_pattern: Optional[AlternatingSessionPattern] = None
lab_pattern: Optional[LabSessionPattern] = None

@model_validator(mode="after")
def validate_only_one_pattern(cls, model):
Expand All @@ -59,56 +75,24 @@ def validate_only_one_pattern(cls, model):
raise ValueError(
"For type='alternating', " "only alternating_pattern must be set"
)
elif model.type == "lab":
if (
not model.lab_pattern
or model.fixed_pattern
or model.alternating_pattern
):
raise ValueError("For type='lab', only lab_pattern must be set")
return model


# ========== Course ==========
class Course(BaseModel):
"""
Represents a course that may or may not include a lab.
Lab courses must define a lab slot and cannot have a session pattern.
Non-lab courses must define a session pattern and cannot have a lab slot.
"""
# ========== StudentGroup ==========
class StudentGroup(BaseModel):
"""Represents a group of students (e.g., a class or cohort)."""

id: int
name: str
code: str
instructor_id: int
has_lab: bool = False
lab_slot: Optional[TimeSlot] = None
lab_room_id: Optional[int] = None
session_pattern: Optional[SessionPattern] = None

@model_validator(mode="after")
def validate_course(cls, model):
if model.has_lab:
if not model.lab_slot:
raise ValueError("Lab slot must be defined for lab courses")
if model.session_pattern:
raise ValueError("Lab courses must not have session pattern")
else:
if not model.session_pattern:
raise ValueError("Non-lab courses must have a session pattern")
if model.lab_slot:
raise ValueError("Non-lab courses must not have lab slot")
return model


class CourseOffering(BaseModel):
"""
Represents the offering of a course by an instructor for a specific subgroup.
Allows multiple instructors to offer the same course to different subgroups,
and a single instructor to teach the same course in multiple subgroups.

Attributes:
course_id (int): Identifier of the course being offered.
instructor_id (int): Identifier of the instructor teaching the course.
sub_group (int): Sub-group number within the course offering. Default is 1.
"""

course_id: int
instructor_id: int
sub_group: int = 1 # Default subgroup is 1
name: str # e.g., "CS-401"
major: str # e.g., "Computer Engineering"
entry_year: int # e.g., 2022


# ========== Student ==========
Expand All @@ -117,17 +101,7 @@ class Student(BaseModel):

full_name: str
student_number: str # e.g., "401234567"
group_id: int # Refers to StudentGroup.id


# ========== StudentGroup ==========
class StudentGroup(BaseModel):
"""Represents a group of students (e.g., a class or cohort)."""

id: int
name: str # e.g., "CS-401"
major: str # e.g., "Computer Engineering"
entry_year: int # e.g., 2022
group_id: StudentGroup # Refers to StudentGroup.id


# ========== Instructor ==========
Expand All @@ -150,17 +124,65 @@ class Room(BaseModel):
is_lab: bool = False


# ========== Course ==========
class Course(BaseModel):
"""
Represents a course that may or may not include a lab.
Lab courses must define a lab slot and cannot have a session pattern.
Non-lab courses must define a session pattern and cannot have a lab slot.
"""

id: int
name: str
code: str
# has_lab: bool = False
# lab_room_id: Optional[int] = None
session_pattern: Optional[SessionPattern] = None


class CourseOffering(BaseModel):
"""
Represents the offering of a course by an instructor for a specific subgroup.
Allows multiple instructors to offer the same course to different subgroups,
and a single instructor to teach the same course in multiple subgroups.

Attributes:
course_id (int): Identifier of the course being offered.
instructor_id (int): Identifier of the instructor teaching the course.
sub_group (int): Sub-group number within the course offering. Default is 1.
"""

course_id: Course
instructor_id: Instructor
sub_group: int = 1 # Default subgroup is 1


# ========== Scheduled Session & Timetable ==========
class ScheduledSession(BaseModel):
"""Represents a scheduled class session (theory or lab)."""

course_id: int
group_id: int
instructor_id: int
room_id: int
course_id: Course
group_id: StudentGroup
instructor_id: Instructor
room_id: Room
slot: TimeSlot
type: Literal["theory", "lab"]

@model_validator(mode="after")
def validate_slot(cls, model):
if model.type == "lab" and not model.course_id.session_pattern:
raise ValueError("Lab sessions must have a lab session pattern defined")
elif model.type == "theory" and not model.course_id.session_pattern:
raise ValueError("Theory sessions must have a session pattern defined")
elif model.type not in ("theory", "lab"):
raise ValueError("Session type must be either 'theory' or 'lab'")

if model.type == "lab" != model.course_id.session_pattern.type:
raise ValueError("The Type of Pattern must match the session type")
if model.type == "theory" and model.course_id.session_pattern.type == "lab":
raise ValueError("Theory sessions cannot have a lab session pattern")
return model


class Timetable(BaseModel):
"""Represents the final schedule as a list of sessions."""
Expand Down
Empty file removed tests/unit/test_constraints.py
Empty file.
Loading