succesfull testing
This commit is contained in:
171
controller/solver/placement_board.py
Normal file
171
controller/solver/placement_board.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
PlacementBoard: shared clash-tracking state and low-level placement
|
||||
primitives. Owns the room/instructor/batch busy-maps and the accumulated
|
||||
session list. Has zero scheduling policy of its own — every phase module
|
||||
(phase1_general, phase2_labs, phase3_remaining) reads and writes the same
|
||||
board, which is what makes each later phase automatically see earlier
|
||||
phases' placements as fixed/locked.
|
||||
"""
|
||||
|
||||
from . import grid
|
||||
|
||||
|
||||
class PlacementBoard:
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
self.classrooms = [r for r in data["rooms"] if r["room_type"] == "classroom"]
|
||||
self.labs = [r for r in data["rooms"] if r["room_type"] == "lab"]
|
||||
|
||||
# busy[(day, slot_index)] -> set of keys already occupied
|
||||
self.room_busy = {}
|
||||
self.instructor_busy = {}
|
||||
self.batch_busy = {}
|
||||
|
||||
self.sessions = []
|
||||
|
||||
# -- serialization -----------------------------------------------------
|
||||
|
||||
def to_state(self):
|
||||
"""Serializes accumulated sessions + busy-maps to a JSON-able dict."""
|
||||
def _dump_busy(busy_map):
|
||||
return {f"{day}|{slot_index}": sorted(values) for (day, slot_index), values in busy_map.items()}
|
||||
|
||||
return {
|
||||
"sessions": self.sessions,
|
||||
"room_busy": _dump_busy(self.room_busy),
|
||||
"instructor_busy": _dump_busy(self.instructor_busy),
|
||||
"batch_busy": _dump_busy(self.batch_busy),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, data, state):
|
||||
"""Rebuilds a PlacementBoard from a dict previously produced by to_state()."""
|
||||
def _load_busy(dumped):
|
||||
busy = {}
|
||||
for key, values in dumped.items():
|
||||
day, slot_index = key.rsplit("|", 1)
|
||||
busy[(day, int(slot_index))] = set(values)
|
||||
return busy
|
||||
|
||||
board = cls(data)
|
||||
board.sessions = list(state["sessions"])
|
||||
board.room_busy = _load_busy(state["room_busy"])
|
||||
board.instructor_busy = _load_busy(state["instructor_busy"])
|
||||
board.batch_busy = _load_busy(state["batch_busy"])
|
||||
return board
|
||||
|
||||
# -- clash-tracking primitives ------------------------------------------
|
||||
|
||||
def is_free(self, day, slot_index, room_id, instructor_code, batch_codes):
|
||||
key = (day, slot_index)
|
||||
if room_id in self.room_busy.get(key, set()):
|
||||
return False
|
||||
if instructor_code in self.instructor_busy.get(key, set()):
|
||||
return False
|
||||
if any(b in self.batch_busy.get(key, set()) for b in batch_codes):
|
||||
return False
|
||||
return True
|
||||
|
||||
def mark_busy(self, day, slot_index, room_id, instructor_code, batch_codes):
|
||||
key = (day, slot_index)
|
||||
self.room_busy.setdefault(key, set()).add(room_id)
|
||||
self.instructor_busy.setdefault(key, set()).add(instructor_code)
|
||||
self.batch_busy.setdefault(key, set()).update(batch_codes)
|
||||
|
||||
def unmark_busy(self, day, slot_index, room_id, instructor_code, batch_codes):
|
||||
key = (day, slot_index)
|
||||
self.room_busy.get(key, set()).discard(room_id)
|
||||
self.instructor_busy.get(key, set()).discard(instructor_code)
|
||||
for b in batch_codes:
|
||||
self.batch_busy.get(key, set()).discard(b)
|
||||
|
||||
def room_candidates(self, preferred_room_id, room_pool):
|
||||
"""Preferred room first, then the rest of the same-type pool."""
|
||||
preferred = self.data["rooms_by_id"].get(preferred_room_id)
|
||||
rest = [r for r in room_pool if r["room_id"] != preferred_room_id]
|
||||
return ([preferred] if preferred else []) + rest
|
||||
|
||||
# -- session creation ----------------------------------------------------
|
||||
|
||||
def add_session(self, offering, day, slot_indices, room, is_long_lecture=False):
|
||||
course = offering["course"]
|
||||
if is_long_lecture:
|
||||
start, end = grid.LONG_LECTURE_START, grid.LONG_LECTURE_END
|
||||
duration_minutes = 120
|
||||
session_type = "theory"
|
||||
else:
|
||||
start = grid.TIME_SLOTS[slot_indices[0]]["start"]
|
||||
end = grid.TIME_SLOTS[slot_indices[-1]]["end"]
|
||||
duration_minutes = 90 * len(slot_indices)
|
||||
session_type = "lab" if len(slot_indices) > 1 else "theory"
|
||||
session = {
|
||||
"day": day,
|
||||
"slot_indices": slot_indices,
|
||||
"start": start,
|
||||
"end": end,
|
||||
"duration_minutes": duration_minutes,
|
||||
"course_id": offering["course_id"],
|
||||
"course_name": course["course_name"],
|
||||
"course_type": session_type,
|
||||
"is_general": course["is_general"],
|
||||
"section_id": offering["section_id"],
|
||||
"instructor_code": offering["instructor_code"],
|
||||
"instructor_name": offering["instructor_name"],
|
||||
"room_id": room["room_id"],
|
||||
"room_name": room["room_name"],
|
||||
"batch_codes": offering["batch_codes"],
|
||||
}
|
||||
self.sessions.append(session)
|
||||
return session
|
||||
|
||||
def remove_session(self, session):
|
||||
"""Unmarks a previously placed session's slots and removes it from self.sessions."""
|
||||
for slot_index in session["slot_indices"]:
|
||||
self.unmark_busy(
|
||||
session["day"], slot_index,
|
||||
session["room_id"], session["instructor_code"], session["batch_codes"],
|
||||
)
|
||||
self.sessions.remove(session)
|
||||
|
||||
# -- placement helpers shared by phase modules --------------------------
|
||||
|
||||
def place_theory_session(self, offering, room_candidates, day_order):
|
||||
"""Finds and locks a free normal 90-minute single-slot session; returns the session dict or None."""
|
||||
for day in day_order:
|
||||
for slot_index in grid.TEACHING_SLOT_INDICES:
|
||||
for room in room_candidates:
|
||||
if self.is_free(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"]):
|
||||
self.mark_busy(day, slot_index, room["room_id"], offering["instructor_code"], offering["batch_codes"])
|
||||
return self.add_session(offering, day, [slot_index], room)
|
||||
return None
|
||||
|
||||
def place_long_lecture(self, offering, room_candidates, day_order):
|
||||
"""
|
||||
Finds and locks a free 08:30-10:30 long-lecture session. Occupies
|
||||
the same clash-tracking slot index as normal slot 0 since they
|
||||
overlap in wall-clock time. Returns the session dict or None.
|
||||
"""
|
||||
for day in day_order:
|
||||
for room in room_candidates:
|
||||
if self.is_free(day, grid.LONG_LECTURE_SLOT_INDEX, room["room_id"], offering["instructor_code"], offering["batch_codes"]):
|
||||
self.mark_busy(day, grid.LONG_LECTURE_SLOT_INDEX, room["room_id"], offering["instructor_code"], offering["batch_codes"])
|
||||
return self.add_session(offering, day, [grid.LONG_LECTURE_SLOT_INDEX], room, is_long_lecture=True)
|
||||
return None
|
||||
|
||||
def place_lecture(self, offering, room_candidates, day_order):
|
||||
"""Dispatches to the long (120-min) or normal (90-min) lecture placement based on the course's lecture_duration_minutes."""
|
||||
if offering["course"]["lecture_duration_minutes"] == 120:
|
||||
return self.place_long_lecture(offering, room_candidates, day_order)
|
||||
return self.place_theory_session(offering, room_candidates, day_order)
|
||||
|
||||
def place_lab(self, offering, room_candidates, day_order):
|
||||
"""Finds and locks a free 180-minute lab double-slot session; returns the session dict or None."""
|
||||
for day in day_order:
|
||||
for slot_a, slot_b in grid.LAB_SLOT_PAIRS:
|
||||
for room in room_candidates:
|
||||
if self.is_free(day, slot_a, room["room_id"], offering["instructor_code"], offering["batch_codes"]) and \
|
||||
self.is_free(day, slot_b, room["room_id"], offering["instructor_code"], offering["batch_codes"]):
|
||||
self.mark_busy(day, slot_a, room["room_id"], offering["instructor_code"], offering["batch_codes"])
|
||||
self.mark_busy(day, slot_b, room["room_id"], offering["instructor_code"], offering["batch_codes"])
|
||||
return self.add_session(offering, day, [slot_a, slot_b], room)
|
||||
return None
|
||||
Reference in New Issue
Block a user