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

155 lines
5.7 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 — “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 doesnt 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