Commit Β·
ec747a2
1
Parent(s): bf35524
Fix resource replenishment, grader efficiency, seed calibration, prompt strategy
Browse files- baseline/policy.py +75 -80
- server/constants.py +26 -26
- server/grader.py +16 -4
- server/tasks/task_easy.py +12 -17
baseline/policy.py
CHANGED
|
@@ -1,10 +1,4 @@
|
|
| 1 |
# baseline/policy.py
|
| 2 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
-
# LLM-based policy for Cascade Containment.
|
| 4 |
-
# Reads CityObservation, calls LLM via OpenAI client, returns ContainmentAction.
|
| 5 |
-
# Uses environment variables for API configuration as required by hackathon rules.
|
| 6 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
-
|
| 8 |
import os
|
| 9 |
import sys
|
| 10 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
@@ -15,64 +9,91 @@ from openai import OpenAI
|
|
| 15 |
from models import CityObservation, ContainmentAction
|
| 16 |
|
| 17 |
|
| 18 |
-
# ββ Client Setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
-
|
| 20 |
def get_client() -> OpenAI:
|
| 21 |
-
"""
|
| 22 |
-
Initialise OpenAI client from environment variables.
|
| 23 |
-
Required by hackathon rules β never hardcode API keys.
|
| 24 |
-
"""
|
| 25 |
return OpenAI(
|
| 26 |
-
api_key = os.environ.get("HF_TOKEN", ""),
|
| 27 |
base_url = os.environ.get("API_BASE_URL", "https://router.huggingface.co/v1"),
|
| 28 |
)
|
| 29 |
|
| 30 |
|
| 31 |
-
# ββ Prompt Builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
-
|
| 33 |
def build_prompt(obs: CityObservation) -> str:
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
lines = [
|
| 39 |
-
"You are
|
| 40 |
-
"Your goal
|
| 41 |
"",
|
| 42 |
-
f"
|
| 43 |
-
|
| 44 |
"",
|
| 45 |
-
"
|
| 46 |
]
|
| 47 |
|
| 48 |
-
for d in
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
lines.append(
|
| 52 |
-
f"
|
| 53 |
-
f"
|
| 54 |
-
f"growth_hint={d.growth_rate_hint:.2f}, "
|
| 55 |
-
f"hospital={d.hospital_capacity_remaining:.2f}, "
|
| 56 |
-
f"restricted={'yes' if d.restriction_active else 'no'}, "
|
| 57 |
-
f"tested_recently={'yes' if d.tested_recently else 'no'}"
|
| 58 |
)
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
lines += [
|
| 61 |
"",
|
| 62 |
-
"
|
| 63 |
-
|
| 64 |
-
" - 'restrict' : Impose movement restriction in a district (free, but penalised if infection is low)",
|
| 65 |
-
" - 'allocate' : Deploy medical resources to a district (costs 1 resource)",
|
| 66 |
-
"",
|
| 67 |
-
"Strategy hints:",
|
| 68 |
-
" - Prioritise districts in DANGER or with high growth_hint",
|
| 69 |
-
" - Use 'test' on high growth_hint districts to reveal true infection",
|
| 70 |
-
" - Use 'allocate' on the most infected district",
|
| 71 |
-
" - Only 'restrict' districts above 0.2 infection rate",
|
| 72 |
-
" - If resources = 0, you can only use 'restrict'",
|
| 73 |
-
"",
|
| 74 |
-
"Respond with ONLY a JSON object in this exact format:",
|
| 75 |
-
'{"action_type": "allocate", "district_id": 2}',
|
| 76 |
"",
|
| 77 |
"Your decision:",
|
| 78 |
]
|
|
@@ -80,16 +101,13 @@ def build_prompt(obs: CityObservation) -> str:
|
|
| 80 |
return "\n".join(lines)
|
| 81 |
|
| 82 |
|
| 83 |
-
# ββ LLM Call ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
-
|
| 85 |
def call_llm(prompt: str, client: OpenAI) -> str:
|
| 86 |
-
"""Call the LLM and return the raw response string."""
|
| 87 |
response = client.chat.completions.create(
|
| 88 |
model = os.environ.get("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct"),
|
| 89 |
messages = [
|
| 90 |
{
|
| 91 |
"role": "system",
|
| 92 |
-
"content": "You are an epidemic response AI. Always respond with valid JSON only. No explanation."
|
| 93 |
},
|
| 94 |
{
|
| 95 |
"role": "user",
|
|
@@ -97,27 +115,17 @@ def call_llm(prompt: str, client: OpenAI) -> str:
|
|
| 97 |
}
|
| 98 |
],
|
| 99 |
max_tokens = 50,
|
| 100 |
-
temperature = 0.
|
| 101 |
)
|
| 102 |
return (response.choices[0].message.content or "").strip()
|
| 103 |
|
| 104 |
|
| 105 |
-
# ββ Response Parser βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
-
|
| 107 |
def parse_action(response: str, num_districts: int) -> ContainmentAction:
|
| 108 |
-
"""
|
| 109 |
-
Parse LLM response into a ContainmentAction.
|
| 110 |
-
Handles common LLM formatting issues defensively.
|
| 111 |
-
Falls back to a safe default if parsing fails entirely.
|
| 112 |
-
"""
|
| 113 |
valid_types = {"test", "restrict", "allocate"}
|
| 114 |
|
| 115 |
try:
|
| 116 |
-
# Strip markdown code fences if present
|
| 117 |
cleaned = re.sub(r"```(?:json)?|```", "", response).strip()
|
| 118 |
-
|
| 119 |
-
# Extract JSON object if surrounded by other text
|
| 120 |
-
match = re.search(r"\{.*?\}", cleaned, re.DOTALL)
|
| 121 |
if match:
|
| 122 |
cleaned = match.group()
|
| 123 |
|
|
@@ -125,30 +133,17 @@ def parse_action(response: str, num_districts: int) -> ContainmentAction:
|
|
| 125 |
action_type = str(data.get("action_type", "allocate")).lower().strip()
|
| 126 |
district_id = int(data.get("district_id", 0))
|
| 127 |
|
| 128 |
-
# Validate and clamp
|
| 129 |
if action_type not in valid_types:
|
| 130 |
action_type = "allocate"
|
| 131 |
district_id = max(0, min(district_id, num_districts - 1))
|
| 132 |
|
| 133 |
-
return ContainmentAction(
|
| 134 |
-
action_type = action_type,
|
| 135 |
-
district_id = district_id,
|
| 136 |
-
)
|
| 137 |
|
| 138 |
except Exception:
|
| 139 |
-
# Safe fallback β allocate to district 0
|
| 140 |
return ContainmentAction(action_type="allocate", district_id=0)
|
| 141 |
|
| 142 |
|
| 143 |
-
# ββ Main Policy Function ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 144 |
-
|
| 145 |
def get_action(obs: CityObservation, client: OpenAI) -> ContainmentAction:
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
Called by evaluator.py on every step.
|
| 150 |
-
"""
|
| 151 |
-
prompt = build_prompt(obs)
|
| 152 |
-
response = call_llm(prompt, client)
|
| 153 |
-
action = parse_action(response, len(obs.districts))
|
| 154 |
-
return action
|
|
|
|
| 1 |
# baseline/policy.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import os
|
| 3 |
import sys
|
| 4 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
|
|
| 9 |
from models import CityObservation, ContainmentAction
|
| 10 |
|
| 11 |
|
|
|
|
|
|
|
| 12 |
def get_client() -> OpenAI:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
return OpenAI(
|
| 14 |
+
api_key = os.environ.get("API_KEY") or os.environ.get("HF_TOKEN", ""),
|
| 15 |
base_url = os.environ.get("API_BASE_URL", "https://router.huggingface.co/v1"),
|
| 16 |
)
|
| 17 |
|
| 18 |
|
|
|
|
|
|
|
| 19 |
def build_prompt(obs: CityObservation) -> str:
|
| 20 |
+
num_districts = len(obs.districts)
|
| 21 |
+
has_data_lag = num_districts == 6 # only hard task has lagged data
|
| 22 |
+
|
| 23 |
+
sorted_districts = sorted(
|
| 24 |
+
obs.districts,
|
| 25 |
+
key=lambda d: d.reported_infection_rate,
|
| 26 |
+
reverse=True
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
lines = [
|
| 30 |
+
"You are an epidemic response coordinator.",
|
| 31 |
+
"Your goal: reduce infection in all districts and prevent hospital collapse.",
|
| 32 |
"",
|
| 33 |
+
f"Step {obs.current_step}/{obs.max_steps} | Resources remaining: {obs.available_resources}",
|
| 34 |
+
"β οΈ Resources do NOT replenish. Every resource spent is gone permanently.",
|
| 35 |
"",
|
| 36 |
+
"Districts (sorted by infection rate, highest first):",
|
| 37 |
]
|
| 38 |
|
| 39 |
+
for d in sorted_districts:
|
| 40 |
+
if d.reported_infection_rate > 0.4:
|
| 41 |
+
status = "π΄ CRITICAL"
|
| 42 |
+
elif d.reported_infection_rate > 0.2:
|
| 43 |
+
status = "π‘ WARNING"
|
| 44 |
+
else:
|
| 45 |
+
status = "π’ SAFE"
|
| 46 |
+
|
| 47 |
+
if d.hospital_capacity_remaining < 0.3:
|
| 48 |
+
hosp_status = "β οΈ HOSPITAL DANGER"
|
| 49 |
+
elif d.hospital_capacity_remaining < 0.6:
|
| 50 |
+
hosp_status = "hospital LOW"
|
| 51 |
+
else:
|
| 52 |
+
hosp_status = "hospital OK"
|
| 53 |
+
|
| 54 |
+
lag_note = " [DATA IS 3 DAYS OLD]" if has_data_lag else ""
|
| 55 |
lines.append(
|
| 56 |
+
f" D{d.district_id}: {status} infection={d.reported_infection_rate:.2f}{lag_note} "
|
| 57 |
+
f"growth={d.growth_rate_hint:.2f} {hosp_status}({d.hospital_capacity_remaining:.2f})"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
)
|
| 59 |
|
| 60 |
+
lines += [""]
|
| 61 |
+
|
| 62 |
+
if not has_data_lag:
|
| 63 |
+
# Easy and medium: data is accurate, allocate reduces existing infection
|
| 64 |
+
lines += [
|
| 65 |
+
"HOW ACTIONS WORK:",
|
| 66 |
+
" - 'allocate': costs 1 resource, REDUCES existing infection AND slows spread",
|
| 67 |
+
" - 'restrict': FREE, only slows future spread, does NOT reduce infection",
|
| 68 |
+
" - 'test': costs 1 resource, gives accurate data (NOT needed here, data is real-time)",
|
| 69 |
+
"",
|
| 70 |
+
"DECISION RULES (follow in order):",
|
| 71 |
+
"1. If ANY hospital is below 0.3 capacity: 'allocate' on that district IMMEDIATELY.",
|
| 72 |
+
"2. If resources > 0 and any district is CRITICAL (above 0.4): 'allocate' on the highest.",
|
| 73 |
+
"3. If resources > 0 and any district is WARNING (0.2-0.4): 'allocate' on the highest.",
|
| 74 |
+
"4. If resources = 0: 'restrict' on the highest infected district.",
|
| 75 |
+
"5. NEVER use 'test' β data is already accurate.",
|
| 76 |
+
"6. NEVER restrict a SAFE district (below 0.2) β you will be penalised.",
|
| 77 |
+
]
|
| 78 |
+
else:
|
| 79 |
+
# Hard task: data is 3 days old, act on growth_hint
|
| 80 |
+
lines += [
|
| 81 |
+
"HOW ACTIONS WORK:",
|
| 82 |
+
" - 'allocate': costs 1 resource, reduces infection AND slows spread",
|
| 83 |
+
" - 'restrict': FREE, only slows future spread",
|
| 84 |
+
" - 'test': costs 1 resource (NOT recommended β data lag is unavoidable)",
|
| 85 |
+
"",
|
| 86 |
+
"DECISION RULES (follow in order):",
|
| 87 |
+
"1. If ANY hospital is below 0.3 capacity: 'allocate' on that district IMMEDIATELY.",
|
| 88 |
+
"2. If resources > 0: 'allocate' on the district with HIGHEST growth_hint.",
|
| 89 |
+
"3. If resources = 0: 'restrict' on the district with highest growth_hint.",
|
| 90 |
+
"4. NEVER use 'test' β spending resources on data wastes your limited budget.",
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
lines += [
|
| 94 |
"",
|
| 95 |
+
"Respond with ONLY valid JSON. No explanation. Example:",
|
| 96 |
+
'{"action_type": "allocate", "district_id": 0}',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
"",
|
| 98 |
"Your decision:",
|
| 99 |
]
|
|
|
|
| 101 |
return "\n".join(lines)
|
| 102 |
|
| 103 |
|
|
|
|
|
|
|
| 104 |
def call_llm(prompt: str, client: OpenAI) -> str:
|
|
|
|
| 105 |
response = client.chat.completions.create(
|
| 106 |
model = os.environ.get("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct"),
|
| 107 |
messages = [
|
| 108 |
{
|
| 109 |
"role": "system",
|
| 110 |
+
"content": "You are an epidemic response AI. Always respond with valid JSON only. No explanation, no markdown."
|
| 111 |
},
|
| 112 |
{
|
| 113 |
"role": "user",
|
|
|
|
| 115 |
}
|
| 116 |
],
|
| 117 |
max_tokens = 50,
|
| 118 |
+
temperature = 0.1, # Lower temperature for more consistent decisions
|
| 119 |
)
|
| 120 |
return (response.choices[0].message.content or "").strip()
|
| 121 |
|
| 122 |
|
|
|
|
|
|
|
| 123 |
def parse_action(response: str, num_districts: int) -> ContainmentAction:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
valid_types = {"test", "restrict", "allocate"}
|
| 125 |
|
| 126 |
try:
|
|
|
|
| 127 |
cleaned = re.sub(r"```(?:json)?|```", "", response).strip()
|
| 128 |
+
match = re.search(r"\{.*?\}", cleaned, re.DOTALL)
|
|
|
|
|
|
|
| 129 |
if match:
|
| 130 |
cleaned = match.group()
|
| 131 |
|
|
|
|
| 133 |
action_type = str(data.get("action_type", "allocate")).lower().strip()
|
| 134 |
district_id = int(data.get("district_id", 0))
|
| 135 |
|
|
|
|
| 136 |
if action_type not in valid_types:
|
| 137 |
action_type = "allocate"
|
| 138 |
district_id = max(0, min(district_id, num_districts - 1))
|
| 139 |
|
| 140 |
+
return ContainmentAction(action_type=action_type, district_id=district_id)
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
except Exception:
|
|
|
|
| 143 |
return ContainmentAction(action_type="allocate", district_id=0)
|
| 144 |
|
| 145 |
|
|
|
|
|
|
|
| 146 |
def get_action(obs: CityObservation, client: OpenAI) -> ContainmentAction:
|
| 147 |
+
prompt = build_prompt(obs)
|
| 148 |
+
response = call_llm(prompt, client)
|
| 149 |
+
return parse_action(response, len(obs.districts))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/constants.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
| 2 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
# Single source of truth for all numeric configuration in the environment.
|
| 4 |
# Nothing in this file is computed β these are fixed values only.
|
| 5 |
-
# Adjust reward weights here during tuning without touching environment.py.
|
| 6 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
|
| 8 |
import sys
|
|
@@ -15,56 +14,57 @@ TASK_CONFIG = {
|
|
| 15 |
"easy": {
|
| 16 |
"num_districts": 2,
|
| 17 |
"max_steps": 10,
|
| 18 |
-
"resource_pool": 10,
|
| 19 |
-
"data_lag_days": 0,
|
| 20 |
},
|
| 21 |
"medium": {
|
| 22 |
"num_districts": 4,
|
| 23 |
"max_steps": 15,
|
| 24 |
-
"resource_pool": 8,
|
| 25 |
"data_lag_days": 0,
|
| 26 |
},
|
| 27 |
"hard": {
|
| 28 |
"num_districts": 6,
|
| 29 |
"max_steps": 15,
|
| 30 |
-
"resource_pool": 7,
|
| 31 |
-
"data_lag_days": 3,
|
| 32 |
},
|
| 33 |
}
|
| 34 |
|
| 35 |
|
| 36 |
# ββ Infection Thresholds ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
|
| 38 |
-
INFECTION_THRESHOLD = 0.40
|
| 39 |
-
SAFE_THRESHOLD = 0.20
|
| 40 |
-
LOW_THRESHOLD = 0.20
|
| 41 |
-
HOSPITAL_BREACH_POINT = 0.00
|
| 42 |
|
| 43 |
|
| 44 |
# ββ Spread Mechanics ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 45 |
|
| 46 |
-
SPREAD_RATE_MIN = 0.
|
| 47 |
-
SPREAD_RATE_MAX = 0.
|
| 48 |
-
GROWTH_HINT_NOISE = 0.03 #
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
| 53 |
|
| 54 |
-
RESOURCE_REPLENISH =
|
| 55 |
|
| 56 |
|
| 57 |
# ββ Reward Weights ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
|
| 59 |
-
REWARD_INFECTION_PENALTY
|
| 60 |
-
REWARD_HOSPITAL_BREACH
|
| 61 |
-
REWARD_EARLY_CONTAINMENT
|
| 62 |
-
REWARD_UNNECESSARY_RESTRICTION = -0.20
|
| 63 |
-
REWARD_CORRECT_PRIORITISATION
|
| 64 |
|
| 65 |
|
| 66 |
# ββ Episode Terminal Conditions βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
|
| 68 |
-
#
|
| 69 |
-
#
|
| 70 |
-
#
|
|
|
|
| 2 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
# Single source of truth for all numeric configuration in the environment.
|
| 4 |
# Nothing in this file is computed β these are fixed values only.
|
|
|
|
| 5 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
|
| 7 |
import sys
|
|
|
|
| 14 |
"easy": {
|
| 15 |
"num_districts": 2,
|
| 16 |
"max_steps": 10,
|
| 17 |
+
"resource_pool": 10,
|
| 18 |
+
"data_lag_days": 0,
|
| 19 |
},
|
| 20 |
"medium": {
|
| 21 |
"num_districts": 4,
|
| 22 |
"max_steps": 15,
|
| 23 |
+
"resource_pool": 8,
|
| 24 |
"data_lag_days": 0,
|
| 25 |
},
|
| 26 |
"hard": {
|
| 27 |
"num_districts": 6,
|
| 28 |
"max_steps": 15,
|
| 29 |
+
"resource_pool": 7,
|
| 30 |
+
"data_lag_days": 3,
|
| 31 |
},
|
| 32 |
}
|
| 33 |
|
| 34 |
|
| 35 |
# ββ Infection Thresholds ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
|
| 37 |
+
INFECTION_THRESHOLD = 0.40
|
| 38 |
+
SAFE_THRESHOLD = 0.20
|
| 39 |
+
LOW_THRESHOLD = 0.20
|
| 40 |
+
HOSPITAL_BREACH_POINT = 0.00
|
| 41 |
|
| 42 |
|
| 43 |
# ββ Spread Mechanics ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 44 |
|
| 45 |
+
SPREAD_RATE_MIN = 0.05 # Slowest possible spread rate per day
|
| 46 |
+
SPREAD_RATE_MAX = 0.13 # Fastest possible spread rate per day
|
| 47 |
+
GROWTH_HINT_NOISE = 0.03 # Noise on growth_rate_hint (Β± value)
|
| 48 |
+
|
| 49 |
+
TREATMENT_REDUCTION = 0.08 # Allocating reduces existing infection this step
|
| 50 |
+
ALLOCATE_REDUCTION = 0.10 # Allocating reduces future spread rate this step
|
| 51 |
+
RESTRICT_REDUCTION = 0.05 # Restricting reduces spread per step
|
| 52 |
+
SPILLOVER_RATE = 0.01 # Infection fraction that spills to adjacent districts
|
| 53 |
|
| 54 |
+
RESOURCE_REPLENISH = 0 # No replenishment β pool is total budget for episode
|
| 55 |
|
| 56 |
|
| 57 |
# ββ Reward Weights ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
|
| 59 |
+
REWARD_INFECTION_PENALTY = -0.50
|
| 60 |
+
REWARD_HOSPITAL_BREACH = -1.00
|
| 61 |
+
REWARD_EARLY_CONTAINMENT = +0.50
|
| 62 |
+
REWARD_UNNECESSARY_RESTRICTION = -0.20
|
| 63 |
+
REWARD_CORRECT_PRIORITISATION = +0.30
|
| 64 |
|
| 65 |
|
| 66 |
# ββ Episode Terminal Conditions βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
|
| 68 |
+
# Success: ALL districts below SAFE_THRESHOLD β speed bonus fires
|
| 69 |
+
# Failure: ANY hospital at HOSPITAL_BREACH_POINT β episode ends immediately
|
| 70 |
+
# Natural: max_steps reached
|
server/grader.py
CHANGED
|
@@ -125,7 +125,12 @@ def grade_trajectory(
|
|
| 125 |
# Fraction of allocate/test actions that targeted districts above threshold.
|
| 126 |
# Rewards directing resources where they're actually needed.
|
| 127 |
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
s for s in trajectory
|
| 130 |
if s.action.action_type in {"allocate", "test"}
|
| 131 |
]
|
|
@@ -133,12 +138,19 @@ def grade_trajectory(
|
|
| 133 |
if resource_actions:
|
| 134 |
correct_actions = 0
|
| 135 |
for step in resource_actions:
|
| 136 |
-
|
| 137 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
correct_actions += 1
|
| 139 |
efficiency_score = correct_actions / len(resource_actions)
|
| 140 |
else:
|
| 141 |
-
efficiency_score = 0.5
|
| 142 |
|
| 143 |
# ββ Component 4: Speed Score ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 144 |
# Rewards finishing faster than max_steps.
|
|
|
|
| 125 |
# Fraction of allocate/test actions that targeted districts above threshold.
|
| 126 |
# Rewards directing resources where they're actually needed.
|
| 127 |
|
| 128 |
+
# ββ Component 3: Efficiency Score ββββββββββββββββββββββββββββββββββββββββ
|
| 129 |
+
# Rewards directing resources to the highest-infected district.
|
| 130 |
+
# Checks which district had the highest infection at each step,
|
| 131 |
+
# then rewards targeting it regardless of whether it crossed threshold.
|
| 132 |
+
|
| 133 |
+
resource_actions = [
|
| 134 |
s for s in trajectory
|
| 135 |
if s.action.action_type in {"allocate", "test"}
|
| 136 |
]
|
|
|
|
| 138 |
if resource_actions:
|
| 139 |
correct_actions = 0
|
| 140 |
for step in resource_actions:
|
| 141 |
+
districts = step.city_state.districts
|
| 142 |
+
if not districts:
|
| 143 |
+
continue
|
| 144 |
+
# Reward targeting the most infected district at this step
|
| 145 |
+
highest_id = max(districts, key=lambda d: d.true_infection_rate).district_id
|
| 146 |
+
if step.action.district_id == highest_id:
|
| 147 |
+
correct_actions += 1
|
| 148 |
+
# Also credit actions on districts above threshold (correct triage)
|
| 149 |
+
elif districts[step.action.district_id].true_infection_rate > INFECTION_THRESHOLD:
|
| 150 |
correct_actions += 1
|
| 151 |
efficiency_score = correct_actions / len(resource_actions)
|
| 152 |
else:
|
| 153 |
+
efficiency_score = 0.5
|
| 154 |
|
| 155 |
# ββ Component 4: Speed Score ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 156 |
# Rewards finishing faster than max_steps.
|
server/tasks/task_easy.py
CHANGED
|
@@ -1,14 +1,8 @@
|
|
| 1 |
# server/tasks/task_easy.py
|
| 2 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
-
# Easy task: 2 districts, 1 outbreak, accurate real-time data.
|
| 4 |
-
# Agent should learn to test the infected district, restrict it,
|
| 5 |
-
# and allocate resources. A straightforward strategy scores 0.7β0.9.
|
| 6 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
-
|
| 8 |
import sys
|
| 9 |
import os
|
| 10 |
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
| 11 |
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
|
| 12 |
|
| 13 |
from models import CityState
|
| 14 |
from server.utils import generate_districts
|
|
@@ -19,15 +13,16 @@ from server.constants import TASK_CONFIG
|
|
| 19 |
class EasyTask(BaseTask):
|
| 20 |
|
| 21 |
name = "easy"
|
| 22 |
-
num_districts = TASK_CONFIG["easy"]["num_districts"]
|
| 23 |
-
max_steps = TASK_CONFIG["easy"]["max_steps"]
|
| 24 |
-
resource_pool = TASK_CONFIG["easy"]["resource_pool"]
|
| 25 |
-
data_lag_days = TASK_CONFIG["easy"]["data_lag_days"]
|
| 26 |
|
| 27 |
def build_initial_state(self) -> CityState:
|
| 28 |
-
#
|
| 29 |
-
#
|
| 30 |
-
|
|
|
|
| 31 |
|
| 32 |
return CityState(
|
| 33 |
day = 0,
|
|
@@ -36,8 +31,8 @@ class EasyTask(BaseTask):
|
|
| 36 |
data_lag_days = self.data_lag_days,
|
| 37 |
max_steps = self.max_steps,
|
| 38 |
districts = generate_districts(
|
| 39 |
-
num_districts
|
| 40 |
-
seed_infections
|
| 41 |
),
|
| 42 |
infection_history = [],
|
| 43 |
)
|
|
|
|
| 1 |
# server/tasks/task_easy.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import sys
|
| 3 |
import os
|
| 4 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
| 5 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
|
| 6 |
|
| 7 |
from models import CityState
|
| 8 |
from server.utils import generate_districts
|
|
|
|
| 13 |
class EasyTask(BaseTask):
|
| 14 |
|
| 15 |
name = "easy"
|
| 16 |
+
num_districts = TASK_CONFIG["easy"]["num_districts"]
|
| 17 |
+
max_steps = TASK_CONFIG["easy"]["max_steps"]
|
| 18 |
+
resource_pool = TASK_CONFIG["easy"]["resource_pool"]
|
| 19 |
+
data_lag_days = TASK_CONFIG["easy"]["data_lag_days"]
|
| 20 |
|
| 21 |
def build_initial_state(self) -> CityState:
|
| 22 |
+
# D0 has a visible outbreak in WARNING-CRITICAL boundary.
|
| 23 |
+
# Seed is high enough that one allocation cannot instantly contain it β
|
| 24 |
+
# agent must sustain 3+ allocations while also managing D1's growth.
|
| 25 |
+
seed_infections = [0.32, 0.06]
|
| 26 |
|
| 27 |
return CityState(
|
| 28 |
day = 0,
|
|
|
|
| 31 |
data_lag_days = self.data_lag_days,
|
| 32 |
max_steps = self.max_steps,
|
| 33 |
districts = generate_districts(
|
| 34 |
+
num_districts = self.num_districts,
|
| 35 |
+
seed_infections = seed_infections,
|
| 36 |
),
|
| 37 |
infection_history = [],
|
| 38 |
)
|