2025-09-03 03:34:30 -07:00

216 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (cant 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 weve 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
# Dont 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