succesfull testing
This commit is contained in:
116
controller/solver/phase2_labs.py
Normal file
116
controller/solver/phase2_labs.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Phase 2: labs.
|
||||
|
||||
Slightly more advanced than Phase 1 — places every lab offering's weekly
|
||||
labs greedily first, against Phase 1's already-fixed placements. When a
|
||||
lab can't be placed, this backtracks *recursively*: it bumps an
|
||||
already-placed lab from earlier in this phase that shares the contested
|
||||
resource (same instructor or same lab room), and tries to re-place the
|
||||
stuck lab; if the bumped lab then can't find a new slot either, that
|
||||
bumped lab is itself backtracked the same way (bump one of *its*
|
||||
resource-sharing predecessors), and so on, until either everything
|
||||
settles or the attempt budget is exhausted. Phase 1's sessions are never
|
||||
touched — only Phase 2's own placements are ever moved.
|
||||
"""
|
||||
|
||||
from . import grid
|
||||
|
||||
MAX_BACKTRACK_ATTEMPTS = 2000
|
||||
|
||||
|
||||
def run(board, data):
|
||||
"""Places every lab offering's weekly labs. Returns this phase's sessions."""
|
||||
phase_start = len(board.sessions)
|
||||
offerings = [o for o in data["offerings"] if not o["course"]["is_general"] and o["course"]["labs_per_week"] > 0]
|
||||
|
||||
placed_so_far = [] # [(offering, session), ...] placed so far in this phase
|
||||
attempts = [0]
|
||||
|
||||
for offering in offerings:
|
||||
course = offering["course"]
|
||||
room_candidates = board.room_candidates(course["lab_room_id"], board.labs)
|
||||
for _ in range(course["labs_per_week"]):
|
||||
session = _place_lab_with_backtracking(board, offering, room_candidates, placed_so_far, attempts)
|
||||
if session is None:
|
||||
print(f"WARNING: could not place a lab for course {offering['course_id']} section {offering['section_id']} ({offering['batch_codes']}) even after backtracking")
|
||||
else:
|
||||
placed_so_far.append((offering, session))
|
||||
|
||||
return board.sessions[phase_start:]
|
||||
|
||||
|
||||
def _place_lab_with_backtracking(board, offering, room_candidates, placed_so_far, attempts):
|
||||
"""
|
||||
Tries the plain first-fit placement first; if that fails, tries
|
||||
bumping each resource-sharing predecessor (most recent first) out of
|
||||
the way and recursively re-seating it, backing out cleanly if a given
|
||||
bump doesn't lead anywhere. Returns the newly placed session dict, or
|
||||
None if no arrangement works within the attempt budget.
|
||||
"""
|
||||
session = board.place_lab(offering, room_candidates, grid.DAYS)
|
||||
if session is not None:
|
||||
return session
|
||||
|
||||
# Snapshot the current candidates once — recursive calls will extend
|
||||
# placed_so_far with their own successful re-seatings, but we only
|
||||
# want to consider *this call's* view of what existed when we started.
|
||||
candidate_room_ids = {r["room_id"] for r in room_candidates}
|
||||
candidates = [
|
||||
(other_offering, other_session)
|
||||
for other_offering, other_session in placed_so_far
|
||||
if _shares_contested_resource(offering, other_session, candidate_room_ids)
|
||||
]
|
||||
|
||||
for other_offering, other_session in reversed(candidates):
|
||||
if attempts[0] >= MAX_BACKTRACK_ATTEMPTS:
|
||||
return None
|
||||
attempts[0] += 1
|
||||
|
||||
if (other_offering, other_session) not in placed_so_far:
|
||||
# Already moved by an earlier (failed) branch of this search.
|
||||
continue
|
||||
|
||||
placed_so_far.remove((other_offering, other_session))
|
||||
board.remove_session(other_session)
|
||||
|
||||
session = board.place_lab(offering, room_candidates, grid.DAYS)
|
||||
if session is not None:
|
||||
other_room_candidates = board.room_candidates(other_offering["course"]["lab_room_id"], board.labs)
|
||||
new_other_session = _place_lab_with_backtracking(board, other_offering, other_room_candidates, placed_so_far, attempts)
|
||||
if new_other_session is not None:
|
||||
placed_so_far.append((other_offering, new_other_session))
|
||||
return session
|
||||
# Could not reseat the bumped lab even with recursion — undo
|
||||
# our placement and put the bump back exactly as it was.
|
||||
board.remove_session(session)
|
||||
placed_so_far.append((other_offering, _restore_lab(board, other_offering, other_session)))
|
||||
continue
|
||||
|
||||
# Bumping didn't free a usable slot for `offering` — restore and
|
||||
# try the next candidate.
|
||||
placed_so_far.append((other_offering, _restore_lab(board, other_offering, other_session)))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _restore_lab(board, offering, session):
|
||||
"""Re-marks and re-adds a lab session dict exactly as it was."""
|
||||
room = board.data["rooms_by_id"][session["room_id"]]
|
||||
for slot_index in session["slot_indices"]:
|
||||
board.mark_busy(session["day"], slot_index, session["room_id"], session["instructor_code"], session["batch_codes"])
|
||||
return board.add_session(offering, session["day"], session["slot_indices"], room)
|
||||
|
||||
|
||||
def _shares_contested_resource(offering, other_session, candidate_room_ids):
|
||||
"""
|
||||
True if bumping other_session could plausibly free a slot offering
|
||||
needs: shares the instructor, a usable lab room, or one of offering's
|
||||
own batches (a batch can only be in one place at a time, so another
|
||||
session competing for that batch's slot is a real contested resource
|
||||
too, not just instructor/room).
|
||||
"""
|
||||
if other_session["instructor_code"] == offering["instructor_code"]:
|
||||
return True
|
||||
if other_session["room_id"] in candidate_room_ids:
|
||||
return True
|
||||
return bool(set(other_session["batch_codes"]) & set(offering["batch_codes"]))
|
||||
Reference in New Issue
Block a user