# policy.py — anti-stall, rare+overlap–reserved, feasibility-first # # Fix for your “stuck at ~550 admits” case: # • Keep hard feasibility (can’t paint into a corner). # • Explicitly reserve seats for the three true bottlenecks: # – queer_friendly (Q), vinyl_collector (V), and the GI overlap # (german_speaker ∩ international) which must be ≥ 450. # • Use *pacing* only as a soft gate; never let it deadlock: # – While Q/V/GI-overlap are behind pace, prefer them. # – Allow other helpers ONLY if, after admitting, there is comfortable # cushion beyond the reserved seats for Q/V/GI-overlap. # – If we still see a long rejection streak, auto-relax the pacing gate # (base + reserves feasibility still enforced) to break droughts. # # Guarantees: # • Reject non-helpers. # • Per-attribute feasibility after each admit. # • GI-overlap feasibility (and aggregated rare+overlap reserve) after each admit. # • Anti-stall: pacing can’t lock the policy into rejecting forever. # # Toggle DEBUG=True for verbose reasoning. import sys from math import ceil from typing import Dict, List DEBUG = False # set True for step-by-step logs # Attribute names for Scenario 3 U = "underground_veteran" I = "international" F = "fashion_forward" Q = "queer_friendly" V = "vinyl_collector" G = "german_speaker" # --- Module state (persists across calls) --- _STEP = 0 _REJECT_STREAK = 0 _GI_BOTH_ADMITTED = 0 # admits with BOTH german_speaker AND international # --- Tunables --- PACE_SLACK = 0 # how many below on-track we tolerate RELAX_STREAK = 400 # after this many consecutive rejects, relax pacing gate CUSHION_FRACTION = 0.05 # 5% of remaining seats as cushion when behind pace MIN_CUSHION = 2 # at least this cushion when behind pace 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(attrs: Dict[str, bool], need: Dict[str, int]) -> List[str]: return [a for a, n in need.items() if n > 0 and attrs.get(a, False)] def _pace_required(target: int, admitted: int, cap: int) -> int: return ceil(target * admitted / max(1, cap)) def _feasible_per_attribute(attrs: Dict[str, bool], need: Dict[str, int], S: int) -> bool: S_after = S - 1 for a, n in need.items(): rem = n - (1 if attrs.get(a, False) else 0) if rem < 0: rem = 0 if S_after < rem: return False return True def _gi_overlap_total_min(mins: Dict[str, int], cap: int) -> int: # Minimum number of admits that must be BOTH german & international to satisfy both minima in cap seats return max(0, mins.get(G, 0) + mins.get(I, 0) - cap) def _reserve_after_admit(attrs: Dict[str, bool], need_Q: int, need_V: int, need_GI: int) -> int: # Remaining reserved seats needed for Q, V, and GI-overlap AFTER this admission q_after = max(0, need_Q - (1 if attrs.get(Q, False) else 0)) v_after = max(0, need_V - (1 if attrs.get(V, False) else 0)) gi_after = max(0, need_GI - (1 if (attrs.get(G, False) and attrs.get(I, False)) else 0)) return q_after + v_after + gi_after def decide(attributes: Dict[str, bool], tallies: Dict[str, int], mins: Dict[str, int], admitted_count: int, venue_cap: int) -> bool: global _STEP, _REJECT_STREAK, _GI_BOTH_ADMITTED _STEP += 1 S = venue_cap - admitted_count # remaining seats need = _need(mins, tallies) total_need = sum(need.values()) # Admit freely when all minima are satisfied. if total_need == 0: _REJECT_STREAK = 0 _log("All minima satisfied -> admit") return True # Must help at least one unmet attribute. helps = _helpers(attributes, need) if not helps: _REJECT_STREAK += 1 _log("Reject: non-helper") return False # Hard per-attribute feasibility. if not _feasible_per_attribute(attributes, need, S): _REJECT_STREAK += 1 _log("Reject: per-attribute feasibility would break") return False # --- GI-overlap reserve & feasibility --- gi_min_total = _gi_overlap_total_min(mins, venue_cap) # = 450 in this scenario gi_need_now = max(0, gi_min_total - _GI_BOTH_ADMITTED) # Reserved seats for Q, V, and GI-overlap after admitting this person reserved_after = _reserve_after_admit(attributes, need.get(Q, 0), need.get(V, 0), gi_need_now) S_after = S - 1 # Hard reserve gate: we must always be able to fit remaining Q, V, GI-overlap. if S_after < reserved_after: _REJECT_STREAK += 1 _log(f"Reject: violates rare+overlap reserve (S_after={S_after} < reserved={reserved_after})") return False # --- Pacing (soft gate with anti-stall) --- # On-track targets by now: q_pace = _pace_required(mins.get(Q, 0), admitted_count, venue_cap) v_pace = _pace_required(mins.get(V, 0), admitted_count, venue_cap) gi_pace = _pace_required(gi_min_total, admitted_count, venue_cap) behind_Q = tallies.get(Q, 0) + PACE_SLACK < q_pace behind_V = tallies.get(V, 0) + PACE_SLACK < v_pace behind_GI = _GI_BOTH_ADMITTED + PACE_SLACK < gi_pace behind_any = behind_Q or behind_V or behind_GI is_GI_both = bool(attributes.get(G, False) and attributes.get(I, False)) is_Q = bool(attributes.get(Q, False)) is_V = bool(attributes.get(V, False)) if behind_any: # Prefer admitting GI-both or rare (Q/V) when behind pace. if (behind_GI and is_GI_both) or (behind_Q and is_Q) or (behind_V and is_V): if is_GI_both: _GI_BOTH_ADMITTED += 1 _REJECT_STREAK = 0 _log("Accept: behind pace -> prioritized helper (GI-both or Q/V)") return True # Otherwise, allow admitting other helpers only if there's *comfortable* cushion beyond reserves. # Cushion scaled to remaining seats to avoid early over-admitting. cushion_needed = max(MIN_CUSHION, int(S_after * CUSHION_FRACTION)) cushion_have = S_after - reserved_after if cushion_have >= cushion_needed or _REJECT_STREAK >= RELAX_STREAK: if is_GI_both: _GI_BOTH_ADMITTED += 1 _REJECT_STREAK = 0 _log(f"Accept: behind pace but cushion OK (have={cushion_have}≥{cushion_needed}) " f"or relax streak hit ({_REJECT_STREAK>=RELAX_STREAK})") return True # Not prioritized and cushion too small -> reject to conserve headroom. _REJECT_STREAK += 1 _log(f"Reject: behind pace, cushion too small (have={cushion_have}<{cushion_needed})") return False # On pace: admit any helper (reserves already enforced). if is_GI_both: _GI_BOTH_ADMITTED += 1 _REJECT_STREAK = 0 _log(f"Accept: on pace helper {helps}") return True