Skip to content

Feat/room blockout - #105

Merged
MarkFontenot merged 5 commits into
developfrom
feat/room-blockout
Jul 24, 2026
Merged

Feat/room blockout#105
MarkFontenot merged 5 commits into
developfrom
feat/room-blockout

Conversation

@ekremarmagankarakas

Copy link
Copy Markdown
Contributor

Summary

  • Fix merge group scheduling — replaced high-weight edge approach with node contraction before DSATUR coloring, guaranteeing merged courses always share the same time slot
  • Add room blockout feature — users can upload an optional CSV specifying (room, day, block) slots where rooms are unavailable; the scheduler respects these during room assignment
  • Surface unroomed exams — courses that have a time slot but no available room (all rooms blocked) are now tracked, persisted to the DB, and visualized distinctly from unscheduled courses

Room blockout CSV support:

  • RoomBlockoutSchema and RoomBlockoutAdapter with flexible day/block
    parsing (accepts integers or named values like 'Monday', '9AM-11AM')
  • room_blockouts field on SchedulingDataset; DatasetFactory accepts
    optional blockouts_df
  • _assign_rooms skips blocked rooms and tracks unroomed courses in
    ScheduleResult.unassigned so unplaced_exams count is accurate
  • API route and DatasetService accept optional room_blockouts file
  • Precompute blockout_slots into file_paths JSONB metadata at upload
    time so schedule retrieval never re-reads from S3
  • Frontend: optional upload slot with badge, store, and API support
  • Tests: adapter parsing, scheduler enforcement, end-to-end pipeline

Merge group scheduling fix:

  • Replace weight-9999 edge hack with DSATUR graph contraction — edges
    mean must-be-different-color so the old approach prevented merged
    courses from sharing a slot rather than guaranteeing it
  • Contract each merge group into a virtual node, union external edges,
    expand colors back after coloring
  • Sum soft-constraint penalties across all CRNs in a group during slot
    selection so back-to-back and load costs reflect the full group
  • Add algorithm correctness test suite (coloring validity, no hard
    conflicts, merge invariants, determinism)

Frontend:

  • Pass per-slot blocked room counts through schedule API response
    (blockouts field on both generate and retrieve endpoints)
  • CompactView shows blocked room badge per calendar cell
  • DensityView shows blocked room badge alongside conflict badges
  • StatisticsView shows blockout stats card (rooms constrained, total
    blocked slots) when blockouts are present
  • Extract extractTimeFromBlock to shared utils.ts, removing duplicate
    implementations in both calendar views

Other:

  • Run scheduler in asyncio.to_thread (CPU-bound work was blocking the
    event loop)
  • Make instructor, department, and term optional in course schema so
    datasets missing these fields upload successfully

  Room blockout CSV support:
  - RoomBlockoutSchema and RoomBlockoutAdapter with flexible day/block
    parsing (accepts integers or named values like 'Monday', '9AM-11AM')
  - room_blockouts field on SchedulingDataset; DatasetFactory accepts
    optional blockouts_df
  - _assign_rooms skips blocked rooms and tracks unroomed courses in
    ScheduleResult.unassigned so unplaced_exams count is accurate
  - API route and DatasetService accept optional room_blockouts file
  - Precompute blockout_slots into file_paths JSONB metadata at upload
    time so schedule retrieval never re-reads from S3
  - Frontend: optional upload slot with badge, store, and API support
  - Tests: adapter parsing, scheduler enforcement, end-to-end pipeline

  Merge group scheduling fix:
  - Replace weight-9999 edge hack with DSATUR graph contraction — edges
    mean must-be-different-color so the old approach prevented merged
    courses from sharing a slot rather than guaranteeing it
  - Contract each merge group into a virtual node, union external edges,
    expand colors back after coloring
  - Sum soft-constraint penalties across all CRNs in a group during slot
    selection so back-to-back and load costs reflect the full group
  - Add algorithm correctness test suite (coloring validity, no hard
    conflicts, merge invariants, determinism)

  Other:
  - Run scheduler in asyncio.to_thread (CPU-bound work was blocking the
    event loop)
  - Make instructor, department, and term optional in course schema so
    datasets missing these fields upload successfully
  - Pass per-slot blocked room counts through schedule API response
    (blockouts field on both generate and retrieve endpoints)
  - CompactView shows blocked room badge per calendar cell
  - DensityView shows blocked room badge alongside conflict badges
  - StatisticsView shows blockout stats card (rooms constrained, total
    blocked slots) when blockouts are present
  - Extract extractTimeFromBlock to shared utils.ts, removing duplicate
    implementations in both calendar views
@MarkFontenot
MarkFontenot changed the base branch from main to develop July 22, 2026 16:08
…counts

Generate 500'd on any over-capacity merge group: _build_generation_response
received merges=None (caller omitted it) and called merges.get() in the
unscheduled-merge loop. The summary counts also diverged from the retrieve
path -- generate counted only unroomed exams (len(result.unassigned)) while
retrieve counted unroomed + null-slot unscheduled-merge rows.

- Pass merges into _build_generation_response (+ defensive merges = merges or {})
- Add _summarize_placement() computing num_classes/unplaced_exams to match the
  row-based definition in _calculate_summary_stats
- Add regression tests incl. the generate==retrieve invariant

Pre-existing on develop; fixed here because feat/room-blockout's spec requires
accurate unplaced_exams and surfacing of unscheduled exams.

Refs EXENG-14. num_students parity tracked in EXENG-15.
@MarkFontenot

MarkFontenot commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Pushed a standalone fix commit (fb4ce4b) for a pre-existing crash + count divergence that this feature's spec depends on — EXENG-14.

POST /api/schedule/generate/{id} 500'd on any over-capacity merge group: _build_generation_response received merges=None (caller omitted it) and called merges.get() in the unscheduled-merge loop. Separately, unplaced_exams/num_classes diverged between generate (counted only result.unassigned) and retrieve (_calculate_summary_stats, which also counts null-slot unscheduled-merge rows).

  • Pass merges into _build_generation_response (+ defensive merges = merges or {}).
  • New _summarize_placement() computes counts matching the row-based _calculate_summary_stats.
  • Regression tests: backend/tests/services/schedule/test_schedule_summary.py (incl. the generate==retrieve invariant).

Verified live: generate now 200; generate and retrieve both report unplaced_exams=2, num_classes=5 on the repro (was 500 / would've been 0 vs 2).

Pre-existing on develop; fixed here because goal #3 (accurate unplaced_exams, surfacing unscheduled exams) can't ship without it. num_students parity is a separate follow-up (EXENG-15).

@MarkFontenot

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@MarkFontenot
MarkFontenot merged commit 61e1257 into develop Jul 24, 2026
@MarkFontenot
MarkFontenot deleted the feat/room-blockout branch July 24, 2026 09:01
@linear

linear Bot commented Jul 24, 2026

Copy link
Copy Markdown

EXENG-14

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants