Commit 74292b78 by Prayush Kumar

APP: upload a first edition, untested so far

parent da3d6411
# ICTS Housing Cell - Allocation Portal Web GUI
A responsive, responsive Web GUI dashboard application designed to automate the housing lottery allocation process for Postdoctoral Fellows and Graduate Students based on the priority pool policies of the ICTS Housing Cell.
---
## 1. Directory Structure
```text
2026/app/
├── allocator.py # Core allocation and priority pooling rules engine
├── server.py # Flask HTTP Web Server handling JSON APIs
├── run.sh # Startup script to initialize the application via terminal
├── README.md # Usage instructions and logic details (this file)
└── templates/
└── index.html # HTML5 Single Page Application Web Dashboard UI
```
---
## 2. Requirements
The application runs using the system's base conda environment which contains all required dependencies:
- **Python 3**
- **Flask**
- **Pandas**
- **Numpy**
---
## 3. How to Run the Web Application
1. Open your terminal.
2. Run the startup script directly:
```bash
./2026/app/run.sh
```
3. Open your browser and navigate to:
[http://localhost:5000](http://localhost:5000)
---
## 4. Web Dashboard Features
- **Toggle Mode**: Switch seamlessly between **Postdocs** and **Graduate Students** allocation models.
- **CSV Response Upload**: Drag-and-drop the standard responses CSV files (from Google Forms) directly into the portal.
- **Interactive Capacity Configuration**: Adjust the available slots for each housing category (such as hostels or apartments) in real time before triggering the lottery.
- **Custom Lottery Seed**: Enter a custom integer seed (e.g. `1780813800` or date seed) to get fully reproducible allotment order.
- **Interactive Results Table**: Browse allocations instantly. Filter, search by candidate name/email, and verify their satisfied preference rank or unallocated status.
- **Raw Export**: Download the generated allotment tables in `.txt` format (compatible with the existing format) or standard `.csv` files.
import csv
import io
import random
import datetime
import numpy as np
import pandas as pd
# Default Housing Mappings and Capacities
POSTDOC_HOUSING_MAPPING = {
0: "On campus Studios",
1: "Hostel 1 non-1BHK",
2: "Hostel 2",
3: "Hostel 3",
4: "Hostel 4",
5: "Hostel 5",
6: "On campus 1BHK",
7: "Hostel 1 1BHK",
}
DEFAULT_POSTDOC_AVAILABILITY = {
0: 6, # Studio on campus
1: 10, # Sharing 2BHK hostel 1
2: 14, # Sharing 2BHK hostel 2
3: 29, # Single room hostel 3
4: 21, # Single room hostel 4
5: 32, # Single room hostel 5
6: 3, # 1 BHK on campus (family only)
7: 2, # 1 BHK hostel 1 (family only)
}
STUDENT_HOUSING_MAPPING = {
0: "On campus (3BHK on sharing)",
1: "Hostel 1 non-1BHK",
2: "Hostel 2 (2BHK: master/non-master)",
3: "Hostel 3 (single rooms)",
4: "Hostel 4 (single rooms)",
5: "Hostel 5 (single rooms)",
6: "On campus 1BHK",
7: "Hostel 1 1BHK",
8: "On campus Studios",
}
DEFAULT_STUDENT_AVAILABILITY = {
0: 3, # Shared on campus (3BHK)
1: 3, # Room hostel 1
2: 11, # Hostel 2
3: 27, # Hostel 3
4: 21, # Hostel 4
5: 27, # Hostel 5
6: 0, # On campus 1BHK
7: 0, # Hostel 1 1BHK
8: 0, # On campus studios
}
def parse_year_to_int(r):
"""Converts student year string into integer year."""
if isinstance(r, (int, float)) and not np.isnan(r):
return int(r)
s = str(r).lower().strip()
if "first" in s or "1" in s or "incoming" in s:
return 1
elif "second" in s or "2" in s:
return 2
elif "third" in s or "3" in s:
return 3
elif "fourth" in s or "4" in s:
return 4
elif "fifth" in s or "5" in s:
return 5
elif "sixth" in s or "6" in s:
return 6
elif "seven" in s or "7" in s:
return 7
return 1
def compute_preferences_from_ranks(housing_ranks):
"""
Given a list of housing ranks [r0, r1, ..., rN] (where 1 is top choice, 99 or NaN is unranked),
converts it into an ordered list of housing option indices [pref0, pref1, ...].
"""
input_pref = list(housing_ranks)
actual_pref_val = 0
pref_order = [-1] * len(input_pref)
for pref_val in range(1, len(input_pref) + 1):
idxs = [i for i, x in enumerate(input_pref) if x == pref_val]
for idx in idxs[::-1]:
pref_order[idx] = actual_pref_val
actual_pref_val += 1
for idx, val in enumerate(input_pref):
if val > len(input_pref) or val <= 0:
if -1 in pref_order:
# Find last occurrence of -1 in pref_order
rev_idx = pref_order[::-1].index(-1)
last_idx = len(pref_order) - 1 - rev_idx
pref_order[last_idx] = actual_pref_val
actual_pref_val += 1
final_prefs = [-1] * len(input_pref)
for idx in range(len(pref_order)):
if pref_order[idx] >= 0 and pref_order[idx] < len(final_prefs):
final_prefs[pref_order[idx]] = idx
# Filter out any remaining -1s if lengths mismatched
return [p for p in final_prefs if p != -1]
def parse_postdoc_csv(df):
"""Parses Postdoc CSV response dataframe into standardized candidates list."""
proc_df = df.copy()
cols = list(proc_df.columns)
# Standardize/combine duplicate preference columns if present
# Column 5: Hostel 1, 6: Hostel 2, 7: Hostel 3, 8: Hostel 4, 9: Hostel 5, 10: Studio
# 12: 1BHK H1, 13: 1BHK campus, 14: Studio campus, 15: H1.1, 16: H2.1, 17: H3.1, 18: H4.1, 19: H5.1
if len(cols) >= 20:
for orig_idx, dup_idx in [(5, 15), (6, 16), (7, 17), (8, 18), (9, 19), (10, 14)]:
if orig_idx < len(cols) and dup_idx < len(cols):
proc_df.iloc[:, orig_idx] = proc_df.iloc[:, orig_idx].combine_first(proc_df.iloc[:, dup_idx])
candidates = []
for idx, row in proc_df.iterrows():
name = str(row.get("Full Name", row.iloc[2] if len(row) > 2 else f"Applicant {idx+1}")).strip()
email = str(row.get("Email Address", row.iloc[1] if len(row) > 1 else "")).strip().lower()
if not name or name.lower() == "nan":
continue
female_resp = str(row.get("Are you a female postdoctoral fellow?", "")).strip().lower()
female = 1 if female_resp == "yes" else 0
acco_type = str(row.get("Type of accommodation preferred", "")).strip().lower()
student_or_postdoc = "postdoc_family" if acco_type == "family" else "postdoc"
# Dependents & disability checks
dep_str = row.get("Mention the number of dependents who will stay with you. (In numeric digits)", 0)
try:
num_dependents = int(float(dep_str)) if pd.notna(dep_str) else 0
except ValueError:
num_dependents = 0
disability_resp = str(row.get("Please indicate if dependents staying with you fall under the following categories.", ""))
disability_check = 1 if (pd.notna(disability_resp) and len(disability_resp.strip()) > 0 and disability_resp.strip().lower() != "nan") else 0
# Ranks for housing 0..7
# 0: Studio campus, 1: H1, 2: H2, 3: H3, 4: H4, 5: H5, 6: 1BHK campus, 7: 1BHK H1
# Extract ranks safely
ranks = []
# Index mapping in PDF form:
# housing0 (Studio): col 10
# housing1 (H1): col 5
# housing2 (H2): col 6
# housing3 (H3): col 7
# housing4 (H4): col 8
# housing5 (H5): col 9
# housing6 (1BHK campus): col 13
# housing7 (1BHK H1): col 12
def get_rank_val(col_idx):
if col_idx < len(row):
val = row.iloc[col_idx]
try:
if pd.notna(val):
return int(float(val))
except (ValueError, TypeError):
pass
return 99
housing_ranks = [
get_rank_val(10), # 0: Studio
get_rank_val(5), # 1: Hostel 1
get_rank_val(6), # 2: Hostel 2
get_rank_val(7), # 3: Hostel 3
get_rank_val(8), # 4: Hostel 4
get_rank_val(9), # 5: Hostel 5
get_rank_val(13), # 6: 1BHK Campus
get_rank_val(12), # 7: 1BHK Hostel 1
]
pref_vector = compute_preferences_from_ranks(housing_ranks)
# Pool classification for postdoc
pool_number = 5
if student_or_postdoc == "postdoc_family":
if (disability_check == 1) and (num_dependents > 0):
pool_number = 1
elif (num_dependents > 2) or ((num_dependents >= 2) and (female == 1)):
pool_number = 2
elif num_dependents >= 2:
pool_number = 3
elif num_dependents == 1:
pool_number = 4
candidates.append({
"id": idx,
"name": name,
"email": email,
"type": "postdoc",
"student_or_postdoc": student_or_postdoc,
"female": female,
"num_dependents": num_dependents,
"disability_check": disability_check,
"housing_ranks": housing_ranks,
"preferences": pref_vector,
"pool": pool_number
})
return candidates
def parse_student_csv(df):
"""Parses Student CSV response dataframe into standardized candidates list."""
proc_df = df.copy()
candidates = []
for idx, row in proc_df.iterrows():
name = str(row.get("Full Name", row.iloc[3] if len(row) > 3 else f"Student {idx+1}")).strip()
email = str(row.get("Email Address", row.iloc[1] if len(row) > 1 else "")).strip().lower()
if not name or name.lower() == "nan":
continue
program = str(row.get("Program Enrolled", row.iloc[4] if len(row) > 4 else "")).strip()
raw_year = row.get("Current Year of Study", row.iloc[5] if len(row) > 5 else 1)
year = parse_year_to_int(raw_year) + 1
female_resp = str(row.get("Are you a female student?", row.iloc[6] if len(row) > 6 else "")).strip().lower()
female = 1 if female_resp == "yes" else 0
# Preference ranks for 6 student categories:
# 0: On campus (col 7)
# 1: Hostel 1 (col 8)
# 2: Hostel 2 (col 9)
# 3: Hostel 3 (col 10)
# 4: Hostel 4 (col 11)
# 5: Hostel 5 (col 12)
def get_rank_val(col_idx):
if col_idx < len(row):
val = row.iloc[col_idx]
try:
if pd.notna(val):
return int(float(val))
except (ValueError, TypeError):
pass
return 99
housing_ranks = [
get_rank_val(7), # 0: On campus 3BHK
get_rank_val(1), # Wait, student col 8 is Hostel 1
get_rank_val(9), # 2: Hostel 2
get_rank_val(10), # 3: Hostel 3
get_rank_val(11), # 4: Hostel 4
get_rank_val(12), # 5: Hostel 5
]
# Let's fix col 8 for Hostel 1
housing_ranks[1] = get_rank_val(8)
# Build preference vector for student choices
# For student, we map the 6 choices directly
input_pref = list(housing_ranks)
pref_order = [-1] * len(input_pref)
for val_idx, val in enumerate(input_pref):
if val <= len(input_pref) and val > 0:
pref_order[val - 1] = val_idx
for val_idx, val in enumerate(input_pref):
if val > len(input_pref) or val <= 0:
if -1 in pref_order:
rev_idx = pref_order[::-1].index(-1)
last_idx = len(pref_order) - 1 - rev_idx
pref_order[last_idx] = val_idx
pref_vector = [p for p in pref_order if p != -1]
# Priority pool for student
# Pool 1: female year 1
# Pool 2: male year 1 OR (PhD >= 5) OR (IPhD >= 6)
# Pool 3: others
clean_prog = program.lower().replace(" ", "")
pool_number = 3
if female == 1 and year == 1:
pool_number = 1
elif (year == 1) or (year >= 5 and clean_prog == "phd") or (year >= 6 and clean_prog == "iphd"):
pool_number = 2
candidates.append({
"id": idx,
"name": name,
"email": email,
"type": "student",
"program": program,
"year": year,
"female": female,
"housing_ranks": housing_ranks,
"preferences": pref_vector,
"pool": pool_number
})
return candidates
def run_lottery(candidates, mode="postdoc", availability=None, seed=None):
"""
Executes housing lottery allocation for given candidates and availability.
"""
if availability is None:
availability = dict(DEFAULT_POSTDOC_AVAILABILITY if mode == "postdoc" else DEFAULT_STUDENT_AVAILABILITY)
else:
availability = dict(availability)
housing_mapping = POSTDOC_HOUSING_MAPPING if mode == "postdoc" else STUDENT_HOUSING_MAPPING
possible_pools = [1, 2, 3, 4, 5] if mode == "postdoc" else [1, 2, 3]
# Handle random seed
if seed is None:
seed = int(datetime.datetime(2026, 6, 7, 12, 0, 0, 0).timestamp())
elif isinstance(seed, str):
try:
seed = int(seed)
except ValueError:
seed = sum(ord(c) for c in seed)
rng = random.Random(seed)
# Group candidates by pool
pool_groups = {p: [] for p in possible_pools}
for c in candidates:
p = c["pool"]
if p in pool_groups:
pool_groups[p].append(c)
allocations = []
unallocated = []
current_capacity = dict(availability)
pool_order_log = {}
for pool_id in possible_pools:
group = pool_groups[pool_id][:]
rng.shuffle(group)
pool_order_log[pool_id] = [c["name"] for c in group]
for cand in group:
assigned = False
for pref_idx, pref_housing in enumerate(cand["preferences"]):
if current_capacity.get(pref_housing, 0) > 0:
current_capacity[pref_housing] -= 1
allocations.append({
"name": cand["name"],
"email": cand["email"],
"pool": cand["pool"],
"housing_num": pref_housing,
"housing_name": housing_mapping.get(pref_housing, f"Option {pref_housing}"),
"preference_rank_satisfied": pref_idx + 1,
"details": cand
})
assigned = True
break
if not assigned:
unallocated.append({
"name": cand["name"],
"email": cand["email"],
"pool": cand["pool"],
"details": cand
})
summary = {
"total_applicants": len(candidates),
"allocated_count": len(allocations),
"unallocated_count": len(unallocated),
"initial_capacity": availability,
"remaining_capacity": current_capacity,
"seed_used": seed,
"mode": mode,
"pool_order_log": pool_order_log
}
return {
"summary": summary,
"allocations": allocations,
"unallocated": unallocated
}
#!/bin/bash
# Navigate to app directory
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd "$DIR"
# Launch Flask server with base Conda Python which contains pandas/numpy
echo "Starting ICTS Housing Lottery Web GUI..."
echo "Open your browser at: http://localhost:5000"
/home/prayush/miniconda3/bin/python server.py
import os
import io
import json
import pandas as pd
from flask import Flask, request, jsonify, render_template, send_file
from allocator import (
parse_postdoc_csv,
parse_student_csv,
run_lottery,
DEFAULT_POSTDOC_AVAILABILITY,
DEFAULT_STUDENT_AVAILABILITY,
POSTDOC_HOUSING_MAPPING,
STUDENT_HOUSING_MAPPING
)
app = Flask(__name__, template_folder='templates')
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/defaults', methods=['GET'])
def get_defaults():
# Return mapping and default availability numbers
return jsonify({
"postdoc": {
"mapping": POSTDOC_HOUSING_MAPPING,
"availability": DEFAULT_POSTDOC_AVAILABILITY
},
"student": {
"mapping": STUDENT_HOUSING_MAPPING,
"availability": DEFAULT_STUDENT_AVAILABILITY
}
})
@app.route('/api/allocate', methods=['POST'])
def allocate():
if 'file' not in request.files:
return jsonify({"error": "No file uploaded"}), 400
file = request.files['file']
mode = request.form.get('mode', 'postdoc')
seed_str = request.form.get('seed', '')
# Custom availability override
availability_override_json = request.form.get('availability', '{}')
try:
raw_avail = json.loads(availability_override_json)
# Convert keys to integers
availability = {int(k): int(v) for k, v in raw_avail.items()}
except Exception as e:
return jsonify({"error": f"Invalid availability structure: {str(e)}"}), 400
# Custom seed conversion
seed = None
if seed_str.strip() != "":
try:
seed = int(seed_str)
except ValueError:
seed = seed_str # fallback to string seed which run_lottery will handle
try:
# Read the file
stream = io.StringIO(file.stream.read().decode("utf-8"), newline=None)
df = pd.read_csv(stream)
except Exception as e:
return jsonify({"error": f"Error parsing CSV file: {str(e)}"}), 400
try:
if mode == "postdoc":
candidates = parse_postdoc_csv(df)
else:
candidates = parse_student_csv(df)
except Exception as e:
return jsonify({"error": f"Error extracting candidates: {str(e)}"}), 400
if not candidates:
return jsonify({"error": "No valid candidates found in CSV file. Check column mappings."}), 400
try:
results = run_lottery(candidates, mode=mode, availability=availability, seed=seed)
except Exception as e:
return jsonify({"error": f"Error executing lottery: {str(e)}"}), 400
# Format output for frontend JSON representation
return jsonify(results)
if __name__ == '__main__':
# Running locally on port 5000
app.run(host='0.0.0.0', port=5000, debug=True)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ICTS Housing Cell Allotment Portal</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0b0f17;
--card-bg: rgba(22, 29, 43, 0.7);
--card-border: rgba(255, 255, 255, 0.08);
--text-main: #f3f4f6;
--text-muted: #9ca3af;
--primary: #6366f1;
--primary-hover: #4f46e5;
--primary-glow: rgba(99, 102, 241, 0.4);
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--card-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Outfit', sans-serif;
background-color: var(--bg-color);
background-image:
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(16, 185, 129, 0.1) 0px, transparent 50%);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
header {
padding: 2rem 4rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--card-border);
background: rgba(11, 15, 23, 0.8);
backdrop-filter: blur(10px);
z-index: 10;
}
.logo-section h1 {
font-size: 1.8rem;
font-weight: 700;
background: linear-gradient(135deg, #ffffff 0%, var(--text-muted) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
.logo-section p {
font-size: 0.9rem;
color: var(--text-muted);
margin-top: 0.2rem;
}
.container {
display: grid;
grid-template-columns: 420px 1fr;
gap: 2rem;
padding: 2rem 4rem;
flex: 1;
max-width: 1800px;
margin: 0 auto;
width: 100%;
}
.glass-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
backdrop-filter: blur(12px);
border-radius: 16px;
padding: 2rem;
box-shadow: var(--card-shadow);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
box-shadow: 0 8px 36px 0 rgba(99, 102, 241, 0.1);
}
h2 {
font-size: 1.4rem;
font-weight: 600;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
h2::before {
content: '';
display: inline-block;
width: 4px;
height: 1.4rem;
background: var(--primary);
border-radius: 2px;
}
/* Form Controls */
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.mode-toggle {
display: grid;
grid-template-columns: 1fr 1fr;
background: rgba(0, 0, 0, 0.2);
padding: 4px;
border-radius: 8px;
border: 1px solid var(--card-border);
margin-bottom: 1.5rem;
}
.mode-btn {
background: transparent;
border: none;
color: var(--text-muted);
padding: 0.8rem;
font-family: inherit;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s ease;
}
.mode-btn.active {
background: var(--primary);
color: white;
box-shadow: 0 0 12px var(--primary-glow);
}
/* Drag and Drop Uploader */
.file-upload-wrapper {
position: relative;
border: 2px dashed var(--card-border);
border-radius: 12px;
padding: 2rem;
text-align: center;
cursor: pointer;
background: rgba(0, 0, 0, 0.15);
transition: all 0.2s ease;
margin-bottom: 1.5rem;
}
.file-upload-wrapper:hover, .file-upload-wrapper.dragover {
border-color: var(--primary);
background: rgba(99, 102, 241, 0.05);
}
.file-upload-wrapper input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.upload-icon {
font-size: 2.5rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.upload-text {
font-size: 0.95rem;
font-weight: 500;
margin-bottom: 0.3rem;
}
.upload-sub {
font-size: 0.8rem;
color: var(--text-muted);
}
/* Capacity Config */
.capacity-list {
max-height: 250px;
overflow-y: auto;
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.15);
margin-bottom: 1.5rem;
}
.capacity-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.6rem 0.8rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.capacity-item:last-child {
border-bottom: none;
}
.capacity-name {
font-size: 0.9rem;
color: var(--text-main);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 240px;
}
.capacity-input {
width: 70px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--card-border);
border-radius: 6px;
color: white;
padding: 0.4rem;
text-align: center;
font-family: inherit;
font-size: 0.9rem;
transition: border-color 0.2s ease;
}
.capacity-input:focus {
border-color: var(--primary);
outline: none;
}
/* Seed Control */
.seed-wrapper {
display: flex;
gap: 0.5rem;
}
.input-text {
flex: 1;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--card-border);
border-radius: 8px;
color: white;
padding: 0.8rem;
font-family: inherit;
font-size: 0.9rem;
transition: border-color 0.2s ease;
}
.input-text:focus {
border-color: var(--primary);
outline: none;
}
.btn {
background: var(--primary);
border: none;
color: white;
padding: 0.8rem 1.5rem;
font-family: inherit;
font-size: 0.95rem;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 14px var(--primary-glow);
}
.btn:hover {
background: var(--primary-hover);
transform: translateY(-1px);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.08);
box-shadow: none;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.15);
}
.run-btn {
width: 100%;
padding: 1rem;
margin-top: 1rem;
background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
font-size: 1rem;
letter-spacing: 0.5px;
}
.run-btn:hover {
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
}
/* Dashboard Right Panel */
.dashboard-content {
display: flex;
flex-direction: column;
gap: 2rem;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
}
.metric-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 1.5rem;
display: flex;
flex-direction: column;
box-shadow: var(--card-shadow);
}
.metric-label {
font-size: 0.85rem;
color: var(--text-muted);
font-weight: 500;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric-value {
font-size: 2rem;
font-weight: 700;
color: var(--text-main);
}
.metric-value.success { color: var(--success); }
.metric-value.warning { color: var(--warning); }
.metric-value.danger { color: var(--danger); }
/* Table Area */
.results-card {
flex: 1;
display: flex;
flex-direction: column;
min-height: 500px;
}
.table-header-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
gap: 1rem;
}
.search-box {
position: relative;
width: 300px;
}
.search-box input {
width: 100%;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--card-border);
border-radius: 8px;
color: white;
padding: 0.7rem 1rem;
font-family: inherit;
font-size: 0.9rem;
}
.action-buttons {
display: flex;
gap: 0.8rem;
}
.table-container {
flex: 1;
overflow-y: auto;
border: 1px solid var(--card-border);
border-radius: 10px;
background: rgba(0, 0, 0, 0.2);
max-height: 550px;
}
table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: 0.9rem;
}
th {
background: rgba(0, 0, 0, 0.4);
padding: 1rem;
font-weight: 600;
color: var(--text-muted);
border-bottom: 1px solid var(--card-border);
position: sticky;
top: 0;
z-index: 5;
}
td {
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
color: var(--text-main);
}
tr:hover td {
background: rgba(255, 255, 255, 0.02);
}
.badge {
display: inline-block;
padding: 0.25rem 0.6rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-pool {
background: rgba(99, 102, 241, 0.15);
color: #a5b4fc;
border: 1px solid rgba(99, 102, 241, 0.3);
}
.badge-rank {
background: rgba(16, 185, 129, 0.15);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.3);
}
/* Modal Overlay for Loading */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(11, 15, 23, 0.85);
backdrop-filter: blur(8px);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.overlay.active {
opacity: 1;
pointer-events: auto;
}
.spinner-wrapper {
text-align: center;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(99, 102, 241, 0.1);
border-left-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1.5rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
font-size: 1.1rem;
font-weight: 500;
letter-spacing: 0.5px;
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6rem 2rem;
text-align: center;
color: var(--text-muted);
}
.empty-icon {
font-size: 3.5rem;
margin-bottom: 1.5rem;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
</style>
</head>
<body>
<header>
<div class="logo-section">
<h1>ICTS Housing Cell</h1>
<p>Lottery & Allocation Automation Portal</p>
</div>
<div class="logo-section" style="text-align: right;">
<p style="font-size: 0.85rem;">System Time</p>
<p id="live-time" style="font-weight: 500; color: white;"></p>
</div>
</header>
<div class="container">
<!-- Configuration Panel -->
<div class="glass-card" style="display: flex; flex-direction: column; gap: 1rem; overflow-y: auto;">
<h2>Configure Lottery</h2>
<div class="mode-toggle">
<button type="button" class="mode-btn active" id="btn-postdoc" onclick="switchMode('postdoc')">Postdocs</button>
<button type="button" class="mode-btn" id="btn-student" onclick="switchMode('student')">Students</button>
</div>
<div class="form-group">
<label>Form Responses CSV</label>
<div class="file-upload-wrapper" id="dropzone">
<div class="upload-icon">📥</div>
<div class="upload-text" id="file-name">Click or drag CSV here</div>
<div class="upload-sub">Only standard form .csv files</div>
<input type="file" id="csv-file" accept=".csv" onchange="handleFileSelect(event)">
</div>
</div>
<div class="form-group">
<label>Housing Category Capacities</label>
<div class="capacity-list" id="capacity-list-container">
<!-- Loaded dynamically -->
</div>
</div>
<div class="form-group" style="display: flex; flex-direction: column; gap: 0.8rem;">
<div>
<label>Convert Datetime to Seed</label>
<div class="seed-wrapper">
<input type="datetime-local" class="input-text" id="datetime-input" style="font-family: inherit;">
<button type="button" class="btn btn-secondary" onclick="convertDatetimeToSeed()">Convert</button>
</div>
</div>
<div>
<label>Lottery RNG Seed</label>
<div class="seed-wrapper">
<input type="text" class="input-text" id="seed-input" placeholder="e.g. 1780813800">
<button type="button" class="btn btn-secondary" onclick="generateRandomSeed()">Gen Random</button>
</div>
</div>
</div>
<button type="button" class="btn run-btn" onclick="executeAllocation()">Run Housing Lottery</button>
</div>
<!-- Dashboard Content -->
<div class="dashboard-content">
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">Total Applicants</div>
<div class="metric-value" id="val-applicants">-</div>
</div>
<div class="metric-card">
<div class="metric-label">Allocated</div>
<div class="metric-value success" id="val-allocated">-</div>
</div>
<div class="metric-card">
<div class="metric-label">Unallocated</div>
<div class="metric-value danger" id="val-unallocated">-</div>
</div>
<div class="metric-card">
<div class="metric-label">Total Vacancies left</div>
<div class="metric-value warning" id="val-vacancies">-</div>
</div>
</div>
<!-- Allocation Table Card -->
<div class="glass-card results-card" id="results-card">
<div class="empty-state" id="results-empty-state">
<div class="empty-icon">📊</div>
<h3>No Allocation Results Ready</h3>
<p style="margin-top: 0.5rem; max-width: 400px; line-height: 1.5;">Upload a form responses CSV and click 'Run Housing Lottery' to perform the allocation engine calculation.</p>
</div>
<div id="results-table-view" style="display: none; height: 100%; flex-direction: column;">
<div class="table-header-controls">
<div class="search-box">
<input type="text" id="search-input" placeholder="Search applicant by name..." oninput="filterResults()">
</div>
<div class="action-buttons">
<button type="button" class="btn btn-secondary" onclick="exportData('csv')">Export CSV</button>
<button type="button" class="btn btn-secondary" onclick="exportData('txt')">Export TXT</button>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Applicant Name</th>
<th>Email Address</th>
<th>Priority Pool</th>
<th>Allocated Housing</th>
<th>Satisfied Preference</th>
</tr>
</thead>
<tbody id="allotment-rows">
<!-- Generated Dynamically -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="overlay" id="loading-overlay">
<div class="spinner-wrapper">
<div class="spinner"></div>
<div class="loading-text">Computing Optimal Allocations...</div>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-top: 0.5rem;">Shuffling pools and executing greedy assignment rules</p>
</div>
</div>
<script>
let appDefaults = {};
let currentMode = 'postdoc';
let activeResults = null;
let selectedFile = null;
// Set Live Time display
function updateTime() {
const now = new Date();
document.getElementById('live-time').innerText = now.toLocaleString('en-US', { hour12: false });
}
setInterval(updateTime, 1000);
updateTime();
// Fetch defaults on page load
async function fetchDefaults() {
try {
// Prefill default datetime
document.getElementById('datetime-input').value = "2026-06-07T12:00";
const res = await fetch('/api/defaults');
appDefaults = await res.json();
switchMode('postdoc');
convertDatetimeToSeed();
} catch(e) {
console.error("Failed to load application defaults", e);
}
}
// Switch postdoc/student modes
function switchMode(mode) {
currentMode = mode;
document.getElementById('btn-postdoc').classList.toggle('active', mode === 'postdoc');
document.getElementById('btn-student').classList.toggle('active', mode === 'student');
const container = document.getElementById('capacity-list-container');
container.innerHTML = '';
const modeDefaults = appDefaults[mode];
if (!modeDefaults) return;
Object.keys(modeDefaults.availability).forEach(key => {
const name = modeDefaults.mapping[key];
const val = modeDefaults.availability[key];
const item = document.createElement('div');
item.className = 'capacity-item';
item.innerHTML = `
<span class="capacity-name" title="${name}">${name}</span>
<input type="number" min="0" class="capacity-input" data-id="${key}" value="${val}">
`;
container.appendChild(item);
});
}
// Generate Random integer seed
function generateRandomSeed() {
const timestamp = Math.floor(Date.now() / 1000);
document.getElementById('seed-input').value = timestamp;
}
// Convert selected datetime to seed timestamp
function convertDatetimeToSeed() {
const dtVal = document.getElementById('datetime-input').value;
if (!dtVal) {
alert("Please select a date and time first.");
return;
}
// Parse local datetime parameters to match Python's timestamp behaviour
const parts = dtVal.split(/[-T:]/); // [year, month, day, hour, minute]
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1; // 0-based index
const day = parseInt(parts[2]);
const hour = parseInt(parts[3]);
const minute = parseInt(parts[4]);
const d = new Date(year, month, day, hour, minute, 0, 0);
const timestamp = Math.floor(d.getTime() / 1000);
document.getElementById('seed-input').value = timestamp;
}
// File drop zone events
const dropzone = document.getElementById('dropzone');
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('dragover');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
selectedFile = e.dataTransfer.files[0];
document.getElementById('file-name').innerText = selectedFile.name;
}
});
function handleFileSelect(e) {
if (e.target.files.length > 0) {
selectedFile = e.target.files[0];
document.getElementById('file-name').innerText = selectedFile.name;
}
}
// Send request to execute allocation engine
async function executeAllocation() {
if (!selectedFile) {
alert("Please select or drop a responses CSV file first.");
return;
}
const loader = document.getElementById('loading-overlay');
loader.classList.add('active');
// Gather capacities
const capacities = {};
const inputs = document.querySelectorAll('.capacity-input');
inputs.forEach(input => {
const id = input.getAttribute('data-id');
const val = parseInt(input.value) || 0;
capacities[id] = val;
});
const seed = document.getElementById('seed-input').value;
const fd = new FormData();
fd.append('file', selectedFile);
fd.append('mode', currentMode);
fd.append('seed', seed);
fd.append('availability', JSON.stringify(capacities));
try {
const response = await fetch('/api/allocate', {
method: 'POST',
body: fd
});
if (!response.ok) {
const errData = await response.json();
alert(errData.error || "Allocation failed. Check CSV structure.");
loader.classList.remove('active');
return;
}
activeResults = await response.json();
renderResults();
} catch (err) {
alert("An error occurred during lottery execution: " + err.toString());
} finally {
loader.classList.remove('active');
}
}
// Render allocation results to dashboard UI
function renderResults() {
if (!activeResults) return;
const summary = activeResults.summary;
document.getElementById('val-applicants').innerText = summary.total_applicants;
document.getElementById('val-allocated').innerText = summary.allocated_count;
document.getElementById('val-unallocated').innerText = summary.unallocated_count;
// Total vacancies remaining calculation
let totalRemaining = 0;
Object.values(summary.remaining_capacity).forEach(v => {
totalRemaining += v;
});
document.getElementById('val-vacancies').innerText = totalRemaining;
// Hide empty state, show table
document.getElementById('results-empty-state').style.display = 'none';
document.getElementById('results-table-view').style.display = 'flex';
renderTableRows(activeResults.allocations, activeResults.unallocated);
}
function renderTableRows(allocations, unallocated, filterText = '') {
const tbody = document.getElementById('allotment-rows');
tbody.innerHTML = '';
const lowerFilter = filterText.toLowerCase();
// Render Allocated rows
allocations.forEach(item => {
if (filterText && !item.name.toLowerCase().includes(lowerFilter) && !item.email.toLowerCase().includes(lowerFilter)) {
return;
}
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="font-weight: 500;">${item.name}</td>
<td style="color: var(--text-muted);">${item.email}</td>
<td><span class="badge badge-pool">Pool ${item.pool}</span></td>
<td style="font-weight: 500; color: var(--success);">${item.housing_name}</td>
<td><span class="badge badge-rank">Pref #${item.preference_rank_satisfied}</span></td>
`;
tbody.appendChild(tr);
});
// Render Unallocated rows
unallocated.forEach(item => {
if (filterText && !item.name.toLowerCase().includes(lowerFilter) && !item.email.toLowerCase().includes(lowerFilter)) {
return;
}
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="font-weight: 500;">${item.name}</td>
<td style="color: var(--text-muted);">${item.email}</td>
<td><span class="badge badge-pool">Pool ${item.pool}</span></td>
<td style="color: var(--danger); font-weight: 500;">Unallotted (No Vacancy)</td>
<td>-</td>
`;
tbody.appendChild(tr);
});
}
function filterResults() {
if (!activeResults) return;
const search = document.getElementById('search-input').value;
renderTableRows(activeResults.allocations, activeResults.unallocated, search);
}
// Export data to CSV/TXT format downloads
function exportData(format) {
if (!activeResults) return;
let fileContent = '';
let mimeType = 'text/plain';
let filename = `housing_allocations_${currentMode}_${Date.now()}`;
if (format === 'csv') {
mimeType = 'text/csv';
filename += '.csv';
fileContent += 'Applicant Name,Email,Priority Pool,Allocated Room Num,Allocated Housing,Preference Rank Satisfied\n';
activeResults.allocations.forEach(a => {
fileContent += `"${a.name}","${a.email}",${a.pool},${a.housing_num},"${a.housing_name}",${a.preference_rank_satisfied}\n`;
});
activeResults.unallocated.forEach(u => {
fileContent += `"${u.name}","${u.email}",${u.pool},-1,"Unallotted",-1\n`;
});
} else {
filename += '.txt';
fileContent += `# Name,Housing Allocation Num,Housing Allocation\n`;
activeResults.allocations.forEach(a => {
fileContent += `${a.name.toLowerCase()},${a.housing_num},${a.housing_name}\n`;
});
activeResults.unallocated.forEach(u => {
fileContent += `${u.name.toLowerCase()},-1,Unallocated\n`;
});
}
const blob = new Blob([fileContent], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Initialize portal defaults on page load
fetchDefaults();
</script>
</body>
</html>
import os
import sys
import pandas as pd
from allocator import parse_postdoc_csv, parse_student_csv, run_lottery
# Paths resolved relative to script location
APP_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(os.path.dirname(APP_DIR), '2026')
def verify_postdocs():
csv_path = os.path.join(DATA_DIR, 'Housing Allotment Preference Form (PDF) 2026-27 (Responses) - Form Responses 1.csv')
txt_path = os.path.join(DATA_DIR, '2026__postdoc_housing_allocations.txt')
df = pd.read_csv(csv_path)
candidates = parse_postdoc_csv(df)
results = run_lottery(candidates, mode='postdoc', seed=1780813800)
# Read expected
expected = {}
with open(txt_path, 'r') as f:
for line in f:
if line.startswith('#') or not line.strip():
continue
parts = line.strip().split(',')
expected[parts[0].lower()] = int(parts[1])
# Read actual
actual = {}
for a in results['allocations']:
actual[a['name'].lower()] = a['housing_num']
for u in results['unallocated']:
actual[u['name'].lower()] = -1
print("Postdoc Shuffled Pool 5 list:")
print("Expected: ['suruj jyoti kalita', 'shanmugapriya prakasam', 'santana majee', 'nimisha pahuja', 'amit kumar', 'savita rani', 'adrian padellaro', 'georg stettinger', 'muktajyoti saha', 'anurag kaushal', 'mahesh gandikota', 'sounak biswas', 'ken kikuchi', 'mainak pal']")
print("Actual:", results['summary']['pool_order_log'].get(5))
mismatches = []
for name, exp_val in expected.items():
if name not in actual:
mismatches.append(f"Missing applicant {name}")
elif actual[name] != exp_val:
mismatches.append(f"Postdoc {name}: expected {exp_val}, got {actual[name]}")
if mismatches:
print("Postdoc mismatches found:")
for m in mismatches:
print(" ", m)
return False
else:
print("Postdoc allocations match expected output exactly!")
return True
def verify_students():
csv_path = os.path.join(DATA_DIR, 'Housing allotment Request form 2026 - Current Graduate students (Responses) - Form Responses 1.csv')
txt_path = os.path.join(DATA_DIR, '2026__student_housing_allocations.txt')
df = pd.read_csv(csv_path)
candidates = parse_student_csv(df)
results = run_lottery(candidates, mode='student', seed=1780813800)
# Read expected
expected = {}
with open(txt_path, 'r') as f:
for line in f:
if line.startswith('#') or not line.strip():
continue
parts = line.strip().split(',')
expected[parts[0].lower().strip().replace(" ", " ")] = int(parts[1])
# Read actual
actual = {}
for a in results['allocations']:
actual[a['name'].lower().strip().replace(" ", " ")] = a['housing_num']
for u in results['unallocated']:
actual[u['name'].lower().strip().replace(" ", " ")] = -1
print("Student Shuffled Pool 2 list:")
print("Expected: ['ankur barsode', 'mrinal jyoti powdel', 'akash maurya', 'jigyasa watwani', 'avi wadhwa', 'ritwik mukherjee', 'ashik h', 'rajarshi chattopadhyay', 'muhammed irshad p', 'santhiya p s', 'bikram pain', 'mayank kumar bijay', 'aditya thorat', 'alorika kar', 'shridhar vinayak', 'tamoghna ray', 'kaustubh singhi']")
print("Actual:", results['summary']['pool_order_log'].get(2))
mismatches = []
for name, exp_val in expected.items():
if name not in actual:
mismatches.append(f"Missing student {name}")
elif actual[name] != exp_val:
mismatches.append(f"Student {name}: expected {exp_val}, got {actual[name]}")
if mismatches:
print("Student mismatches found:")
for m in mismatches:
print(" ", m)
return False
else:
print("Student allocations match expected output exactly!")
return True
if __name__ == '__main__':
print("Executing Student Verification against expected notebook output...")
s_ok = verify_students()
if s_ok:
print("\nSUCCESS: All student allocations match notebook output exactly!")
sys.exit(0)
else:
print("\nFAILURE: Mismatches found in student allocations.")
sys.exit(1)
# ICTS Housing Cell - Allocation Logic & Policy Documentation (2026)
This document details the housing allotment rules, priority pooling policies, preference processing algorithms, and lottery mechanism used by the ICTS Housing Cell for allocating campus housing to **Postdoctoral Fellows (PDFs)** and **Graduate Students (PhD / Int-PhD)**.
---
## 1. Overview & Allotment Workflow
The housing allocation process automates the distribution of limited hostel rooms and campus apartments based on:
1. **Applicant Group**: Postdoctoral Fellows vs. Graduate Students.
2. **Priority Pools**: Institutional policy rules prioritizing medical needs, dependents, gender, and academic seniority.
3. **Preference Vectors**: Applicant choices ranked from 1 (most preferred) downwards.
4. **Lottery Tie-Breaking**: Pseudo-random shuffling within each priority pool using a fixed RNG seed for reproducibility.
5. **Sequential Greedy Allocation**: Candidate-by-candidate allocation in lottery rank order, assigning their highest-ranked available accommodation.
---
## 2. Accommodation Types & Mapping
### Postdoctoral Fellows (Index 0 – 7)
| Index | Accommodation Category | Description & Eligibility |
| :---: | :--- | :--- |
| `0` | On campus Studios | Studio apartments on campus |
| `1` | Hostel 1 non-1BHK | Shared 2BHK apartment in Hostel 1 |
| `2` | Hostel 2 | Shared 2BHK apartment in Hostel 2 |
| `3` | Hostel 3 | Single occupancy room in Hostel 3 |
| `4` | Hostel 4 | Single occupancy room in Hostel 4 |
| `5` | Hostel 5 | Single occupancy room in Hostel 5 |
| `6` | On campus 1BHK | 1BHK apartment on campus (Family / Married only) |
| `7` | Hostel 1 1BHK | 1BHK apartment in Hostel 1 (Family / Married only) |
### Graduate Students (Index 0 – 8)
| Index | Accommodation Category | Description & Eligibility |
| :---: | :--- | :--- |
| `0` | On campus (3BHK sharing) | Shared 3BHK apartment on campus |
| `1` | Hostel 1 non-1BHK | Shared 2BHK apartment in Hostel 1 |
| `2` | Hostel 2 | Shared 2BHK apartment in Hostel 2 (Master/non-master) |
| `3` | Hostel 3 | Single room in Hostel 3 |
| `4` | Hostel 4 | Single room in Hostel 4 |
| `5` | Hostel 5 | Single room in Hostel 5 |
| `6` | On campus 1BHK | Restricted / PDF priority |
| `7` | Hostel 1 1BHK | Restricted / PDF priority |
| `8` | On campus Studios | Restricted / PDF priority |
---
## 3. Preference Parsing & Order Construction
Google Form responses collect numerical ranks for each option (e.g. `1` for top choice, `2` for second, etc.).
- **Unranked Options**: Represented as `NaN` or `99`.
- **Conversion to Preference Vector**:
1. The raw rank list `[r_0, r_1, ..., r_N]` is converted into an ordered list of accommodation indices `[pref_0, pref_1, ...]`.
2. Choices with lower numerical ranks appear earlier in the preference vector.
3. Tied or repeated ranks are ordered deterministically in reverse column order.
4. Unranked choices (rank `99`) are appended at the end of the candidate's preference vector.
---
## 4. Priority Pool Rules
Applicants are grouped into priority pools. Allocations are executed pool by pool in numerical order (Pool 1 gets highest priority, followed by Pool 2, Pool 3, etc.).
### A. Postdoctoral Fellows Priority Pools (Pools 1 to 5)
1. **Pool 1 (Highest Priority - Medical & Disability Family)**:
- Must prefer Family accommodation (`student_or_postdoc == "postdoc_family"`).
- Must have a registered medical/disability condition (`disability_check == 1`).
- Must have at least 1 dependent (`num_dependents > 0`).
2. **Pool 2 (High Family Priority)**:
- Family postdocs with `num_dependents > 2`, OR
- Female family postdocs with `num_dependents >= 2`.
3. **Pool 3 (Family with 2+ Dependents)**:
- Family postdocs with `num_dependents >= 2`.
4. **Pool 4 (Family with 1 Dependent)**:
- Family postdocs with `num_dependents == 1`.
5. **Pool 5 (General Postdocs)**:
- All single postdocs and remaining family postdocs not meeting higher pool criteria.
### B. Graduate Students Priority Pools (Pools 1 to 3)
*Note on Academic Year*: Before the start of the new session in July, student academic years are normalized (e.g. 1st year incoming is year 1).
1. **Pool 1 (Highest Priority - 1st Year Female Students)**:
- Female graduate students entering or in 1st year (`female == 1` AND `year == 1`).
2. **Pool 2 (1st Year Male & Senior PhD Students)**:
- 1st year male students (`year == 1`), OR
- Senior PhD students: PhD year $\ge 5$ OR Int-PhD year $\ge 6$.
3. **Pool 3 (General Graduate Students)**:
- All other registered graduate students (2nd, 3rd, 4th year PhD; 2nd, 3rd, 4th, 5th year Int-PhD).
---
## 5. Lottery & Allocation Algorithm
1. **Random Seed Initialization**:
- A random seed is configured (e.g., standard noon timestamp `datetime(2026, 6, 7, 12, 0, 0)`).
2. **Sequential Pool Execution**:
- For each pool ID in `[1, 2, ...]`:
- **Shuffle Candidates**: Randomize candidate order within the pool using `random.shuffle(pool_candidates)`.
- **Greedy Allocation**: For each candidate in shuffled order:
- Iterate through candidate's preference vector `[pref_0, pref_1, ...]`.
- Assign the first housing category `pref_i` with `remaining_availability[pref_i] > 0`.
- Decrement `remaining_availability[pref_i]` by 1.
- If all preferred housing options are exhausted (capacity 0), the candidate remains unallocated.
3. **Output Generation**:
- Output contains participant name, email, priority pool ID, allocated housing option ID, allocated housing option name, and preference rank satisfied.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment