216 lines
8.3 KiB
Python
216 lines
8.3 KiB
Python
# policy.py — ultra-selective front-loading with a hard “GI-both” (German∩International) reserve
|
||
#
|
||
# Problem you hit:
|
||
# Early admits were too loose, starving the truly binding quotas later
|
||
# (queer_friendly, vinyl_collector, and the necessary overlap of
|
||
# german_speaker ∩ international).
|
||
#
|
||
# What this does:
|
||
# 1) Helper-only + strict feasibility (can’t paint into a corner).
|
||
# 2) A **virtual required-overlap** constraint for (german_speaker, international):
|
||
# GI_BOTH_MIN = max(0, minG + minI − CAP) = 450 in Scenario 3.
|
||
# We maintain a live counter of how many “GI-both” admits we’ve made and
|
||
# run a feasibility check against the remaining seats (hard gate).
|
||
# 3) **Front-load gating**:
|
||
# • While GI-both is behind its *own* pace, admit ONLY:
|
||
# – GI-both candidates, OR
|
||
# – Rare helpers: queer_friendly or vinyl_collector.
|
||
# (No german-only, no international-only, no “easy” helpers.)
|
||
# • If GI-both is on pace but (queer_friendly or vinyl_collector) is behind,
|
||
# admit ONLY rare helpers (or GI-both).
|
||
# 4) After the front-load phase (GI-both pace OK and rare OK), relax to helper-only
|
||
# (still blocking anything that would break either base feasibility or GI-both reserve).
|
||
#
|
||
# Result:
|
||
# Early acceptance rate is intentionally low (lots of rejections) until the
|
||
# binding quotas are on track; this keeps headroom for scarce attributes and
|
||
# the GI overlap you *must* accumulate to hit both 800 Germans and 650
|
||
# Internationals in only 1000 seats.
|
||
#
|
||
# Toggle DEBUG=True for per-decision logs.
|
||
|
||
import sys
|
||
from math import ceil
|
||
from typing import Dict, List
|
||
|
||
DEBUG = False # set True for verbose reasoning
|
||
|
||
# ---- Scenario 3 attribute names ----
|
||
U = "underground_veteran"
|
||
I = "international"
|
||
F = "fashion_forward"
|
||
Q = "queer_friendly"
|
||
V = "vinyl_collector"
|
||
G = "german_speaker"
|
||
|
||
PRIORITY_RARE = (Q, V) # truly scarce attributes to front-load
|
||
PRIORITY_GI_PAIR = (G, I) # the negative-correlated pair requiring a big overlap
|
||
|
||
# Small pacing slack so we don't thrash on single-count fluctuations.
|
||
PACE_SLACK = 0
|
||
|
||
# Module-level state to count admitted intersections we care about.
|
||
_GI_BOTH_ADMITTED = 0 # count of admits who were BOTH german_speaker AND international
|
||
|
||
|
||
def _log(msg: str) -> None:
|
||
if DEBUG:
|
||
print(msg, file=sys.stderr, flush=True)
|
||
|
||
|
||
def _need(mins: Dict[str, int], tallies: Dict[str, int]) -> Dict[str, int]:
|
||
return {a: max(0, mins[a] - tallies.get(a, 0)) for a in mins}
|
||
|
||
|
||
def _helpers(attributes: Dict[str, bool], need: Dict[str, int]) -> List[str]:
|
||
# Attributes this candidate has that are still unmet
|
||
return [a for a, n in need.items() if n > 0 and attributes.get(a, False)]
|
||
|
||
|
||
def _pace_required(total_target: int, admitted_count: int, cap: int) -> int:
|
||
# On-track count by now for a quota of 'total_target'
|
||
return ceil(total_target * admitted_count / max(1, cap))
|
||
|
||
|
||
def _feasible_base(attributes: Dict[str, bool], need: Dict[str, int], remaining_slots: int) -> bool:
|
||
# Classic per-attribute feasibility: after admitting, each remaining shortfall fits in S_after.
|
||
S_after = remaining_slots - 1
|
||
for a, n in need.items():
|
||
rem = n - (1 if attributes.get(a, False) else 0)
|
||
if rem < 0:
|
||
rem = 0
|
||
if S_after < rem:
|
||
return False
|
||
return True
|
||
|
||
|
||
def _feasible_gi_both(is_gi_both: bool, gi_both_min: int, remaining_slots: int) -> bool:
|
||
# Virtual feasibility for required overlap: after admitting, there must be
|
||
# enough seats to still acquire the remaining GI-both admits.
|
||
global _GI_BOTH_ADMITTED
|
||
have_after = _GI_BOTH_ADMITTED + (1 if is_gi_both else 0)
|
||
need_after = max(0, gi_both_min - have_after)
|
||
S_after = remaining_slots - 1
|
||
return S_after >= need_after
|
||
|
||
|
||
def decide(attributes: Dict[str, bool],
|
||
tallies: Dict[str, int],
|
||
mins: Dict[str, int],
|
||
admitted_count: int,
|
||
venue_cap: int) -> bool:
|
||
global _GI_BOTH_ADMITTED
|
||
|
||
S = venue_cap - admitted_count # remaining seats
|
||
need = _need(mins, tallies)
|
||
total_need = sum(need.values())
|
||
|
||
# Admit freely once all minima are met.
|
||
if total_need == 0:
|
||
_log("All minima satisfied -> admit")
|
||
return True
|
||
|
||
# Candidate properties
|
||
hasG = bool(attributes.get(G, False))
|
||
hasI = bool(attributes.get(I, False))
|
||
hasQ = bool(attributes.get(Q, False))
|
||
hasV = bool(attributes.get(V, False))
|
||
is_GI_both = hasG and hasI
|
||
|
||
# Must help at least one STILL-UNMET base attribute.
|
||
helps = _helpers(attributes, need)
|
||
if not helps:
|
||
_log("Reject: non-helper")
|
||
return False
|
||
|
||
# --- Hard feasibility gates ---
|
||
# 1) Base (per-attribute) feasibility
|
||
if not _feasible_base(attributes, need, S):
|
||
_log("Reject: would break per-attribute feasibility")
|
||
return False
|
||
|
||
# 2) Virtual GI-both feasibility
|
||
gi_both_min_total = max(0, mins.get(G, 0) + mins.get(I, 0) - venue_cap) # = 450 here
|
||
if gi_both_min_total > 0 and not _feasible_gi_both(is_GI_both, gi_both_min_total, S):
|
||
_log("Reject: would break GI-both overlap feasibility")
|
||
return False
|
||
|
||
# --- Pacing (front-load) guards ---
|
||
# On-track counts by now for the truly binding quotas and the GI-both overlap.
|
||
q_pace = _pace_required(mins.get(Q, 0), admitted_count, venue_cap)
|
||
v_pace = _pace_required(mins.get(V, 0), admitted_count, venue_cap)
|
||
g_pace = _pace_required(mins.get(G, 0), admitted_count, venue_cap)
|
||
gi_both_pace = _pace_required(gi_both_min_total, admitted_count, venue_cap)
|
||
|
||
q_have = tallies.get(Q, 0)
|
||
v_have = tallies.get(V, 0)
|
||
g_have = tallies.get(G, 0)
|
||
gi_both_have = _GI_BOTH_ADMITTED # exact, tracked by us
|
||
|
||
behind_Q = (q_have + PACE_SLACK) < q_pace
|
||
behind_V = (v_have + PACE_SLACK) < v_pace
|
||
behind_GI_both = (gi_both_have + PACE_SLACK) < gi_both_pace
|
||
behind_G = (g_have + PACE_SLACK) < g_pace
|
||
|
||
# Priority front-load order:
|
||
# 1) GI-both pace
|
||
# 2) Rare (Q, V) pace
|
||
# 3) German pace (ONLY when GI-both is on pace)
|
||
if behind_GI_both:
|
||
# While GI-both is behind, ONLY admit GI-both or rare helpers.
|
||
if is_GI_both:
|
||
_log("Accept: GI-both behind -> take GI-both")
|
||
_GI_BOTH_ADMITTED += 1
|
||
return True
|
||
if (hasQ and need.get(Q, 0) > 0) or (hasV and need.get(V, 0) > 0):
|
||
_log("Accept: GI-both behind -> take rare (Q/V)")
|
||
return True
|
||
_log("Reject: GI-both behind -> conserve seat (no GI-both, no rare)")
|
||
return False
|
||
|
||
# GI-both is on pace now.
|
||
# If rare quotas are behind, only admit rare (or GI-both which is always welcome).
|
||
if behind_Q or behind_V:
|
||
if is_GI_both:
|
||
_log("Accept: rare behind, GI-both bonus")
|
||
_GI_BOTH_ADMITTED += 1
|
||
return True
|
||
if (behind_Q and hasQ and need.get(Q, 0) > 0) or (behind_V and hasV and need.get(V, 0) > 0):
|
||
_log("Accept: rare behind -> take needed rare")
|
||
return True
|
||
_log("Reject: rare behind -> conserve seat (candidate not needed rare)")
|
||
return False
|
||
|
||
# If German is behind (but GI-both is fine), allow German-only as a third-tier priority.
|
||
if behind_G:
|
||
if is_GI_both:
|
||
_log("Accept: German behind, GI-both present")
|
||
_GI_BOTH_ADMITTED += 1
|
||
return True
|
||
if hasG and need.get(G, 0) > 0 and not hasI:
|
||
_log("Accept: German behind -> take German-only")
|
||
return True
|
||
# Don’t pull in International-only here; keep GI balance healthy.
|
||
if hasI and not hasG:
|
||
_log("Reject: German behind -> skip International-only")
|
||
return False
|
||
|
||
# --- Post front-load: Helper-only, with GI-both feasibility guard already enforced ---
|
||
# Still avoid pure International-only unless we either (a) need International
|
||
# and (b) GI-both target is already fully satisfied.
|
||
gi_both_remaining = max(0, gi_both_min_total - _GI_BOTH_ADMITTED)
|
||
if hasI and not hasG and gi_both_remaining > 0:
|
||
# We still owe GI-both; don't consume seats with International-only yet.
|
||
_log("Reject: GI-both remaining -> defer International-only")
|
||
return False
|
||
|
||
# Safe to admit any feasible helper now.
|
||
if is_GI_both:
|
||
_GI_BOTH_ADMITTED += 1
|
||
_log("Accept: on-track helper (GI-both)")
|
||
return True
|
||
|
||
_log(f"Accept: on-track helper {helps}")
|
||
return True
|
||
|