155 lines
5.7 KiB
Python
155 lines
5.7 KiB
Python
# policy.py — “rare-first, German-safe” strategy for Scenario 3
|
||
#
|
||
# Goal:
|
||
# Hit all minima as early as possible by prioritizing the bottlenecks
|
||
# (queer_friendly, vinyl_collector) and protecting the big German quota,
|
||
# while never painting ourselves into a corner.
|
||
#
|
||
# Hard guarantees:
|
||
# • Per-attribute feasibility check on every decision (no dead-ends).
|
||
# • Reject anyone who doesn’t help an unmet attribute (“non-helper”).
|
||
# • Once all minima are satisfied, admit everyone.
|
||
#
|
||
# Bias (what “does this”):
|
||
# • While any of {queer_friendly, vinyl_collector, german_speaker} are unmet:
|
||
# 1) Admit if candidate has queer_friendly or vinyl_collector (rare).
|
||
# 2) Else admit if candidate has german_speaker (protect 800 target),
|
||
# BUT stay feasibility-safe for other mins (esp. international).
|
||
# 3) Otherwise reject even if they help a non-priority min, unless that
|
||
# non-priority attribute has become *critical* by scarcity.
|
||
# • Exploit overlaps: queer∩vinyl, queer/vinyl with German are admitted eagerly.
|
||
# • When rare quotas are safe and German scarcity ≤ 1, admit other helpers.
|
||
#
|
||
# Notes:
|
||
# • This policy may reject some feasible low-priority helpers early to keep
|
||
# headroom aligned with the binding quotas, as requested.
|
||
# • DEBUG=True prints reasoning to stderr.
|
||
|
||
import sys
|
||
from typing import Dict
|
||
|
||
DEBUG = False # set True for verbose reasoning
|
||
|
||
# Scenario 3 stats (iid arrivals)
|
||
P = {
|
||
"underground_veteran": 0.6795,
|
||
"international": 0.5735,
|
||
"fashion_forward": 0.6910,
|
||
"queer_friendly": 0.04614,
|
||
"vinyl_collector": 0.04454,
|
||
"german_speaker": 0.4565,
|
||
}
|
||
|
||
# Priority groups
|
||
RARE = ("queer_friendly", "vinyl_collector")
|
||
GUARD = "german_speaker" # strongly constrained and conflicts with "international"
|
||
|
||
# Scarcity thresholds
|
||
CRITICAL_SCARCITY = 1.0 # need exceeds expected supply in remaining slots
|
||
ALMOST_SAFE_SCARCITY = 0.9 # used to check if rare quotas are basically safe
|
||
|
||
|
||
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 _scarcity(need: Dict[str, int], S: int) -> Dict[str, float]:
|
||
# Scarcity = remaining shortfall divided by expected remaining supply.
|
||
# (>1 means we're behind expectation; <1 means we are ahead/on track.)
|
||
s = {}
|
||
for a, n in need.items():
|
||
exp = max(1e-9, P.get(a, 0.0) * S)
|
||
s[a] = (n / exp) if n > 0 else 0.0
|
||
return s
|
||
|
||
|
||
def _feasible_after_admit(attributes: Dict[str, bool],
|
||
need: Dict[str, int],
|
||
S: int) -> bool:
|
||
# After admitting this candidate we have S_after slots; for every attribute a,
|
||
# the remaining shortfall must fit in those slots.
|
||
S_after = S - 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 decide(attributes: Dict[str, bool],
|
||
tallies: Dict[str, int],
|
||
mins: Dict[str, int],
|
||
admitted_count: int,
|
||
venue_cap: int) -> bool:
|
||
# Remaining slots
|
||
S = venue_cap - admitted_count
|
||
|
||
# Unmet needs & scarcity
|
||
need = _need(mins, tallies)
|
||
total_need = sum(need.values())
|
||
|
||
# If all minima satisfied, admit everyone.
|
||
if total_need == 0:
|
||
_log("All minima met -> admit")
|
||
return True
|
||
|
||
# Helper set = unmet attributes the candidate has
|
||
helps = [a for a, n in need.items() if n > 0 and attributes.get(a, False)]
|
||
if not helps:
|
||
_log("Non-helper -> reject")
|
||
return False
|
||
|
||
# Feasibility is a hard gate
|
||
if not _feasible_after_admit(attributes, need, S):
|
||
_log("Reject: would break per-attribute feasibility")
|
||
return False
|
||
|
||
# Scarcity picture
|
||
sc = _scarcity(need, S)
|
||
|
||
# Are any of the "rare or guard" quotas still unmet?
|
||
rare_unmet = any(need[a] > 0 for a in RARE)
|
||
guard_unmet = need.get(GUARD, 0) > 0
|
||
in_priority_phase = rare_unmet or guard_unmet
|
||
|
||
# If we're in the priority phase, apply the requested bias:
|
||
if in_priority_phase:
|
||
# 1) If candidate helps a rare attribute, admit.
|
||
if any(attributes.get(a, False) and need.get(a, 0) > 0 for a in RARE):
|
||
_log(f"Accept: rare helper { [a for a in RARE if attributes.get(a, False)] }")
|
||
return True
|
||
|
||
# 2) Else if candidate helps German (guard), admit (feasibility already checked).
|
||
if attributes.get(GUARD, False) and guard_unmet:
|
||
_log("Accept: german_speaker to protect 800 target")
|
||
return True
|
||
|
||
# 3) Else candidate helps only non-priority mins. Normally reject here to
|
||
# keep headroom for rare/German — UNLESS that non-priority attribute
|
||
# itself is now critical by scarcity.
|
||
# (This preserves viability for e.g. international/fashion when genuinely at risk.)
|
||
critical_nonprio = [
|
||
a for a in helps
|
||
if (a not in RARE and a != GUARD and sc.get(a, 0.0) >= CRITICAL_SCARCITY)
|
||
]
|
||
if critical_nonprio:
|
||
_log(f"Accept: non-priority helper {critical_nonprio} is critical by scarcity")
|
||
return True
|
||
|
||
# Otherwise reject to conserve headroom for rare/German during priority phase.
|
||
_log(f"Reject: helper only for non-priority {helps} while rare/guard unmet")
|
||
return False
|
||
|
||
# If not in priority phase (rare met and German safe), admit any helper that keeps feasibility.
|
||
# "German safe" defined implicitly by leaving priority phase; still preserve feasibility.
|
||
_log(f"Accept: post-priority helper {helps}")
|
||
return True
|
||
|