Commit Β·
4d57df8
1
Parent(s): fe22c22
Fix treatment/recovery calibration, hospital breach reward, efficiency grader, recalibrate seeds
Browse files- baseline/__pycache__/policy.cpython-313.pyc +0 -0
- epidemic_containment_env.zip +0 -0
- server/constants.py +18 -52
- server/environment.py +2 -1
- server/grader.py +70 -151
- server/tasks/task_easy.py +7 -7
- server/tasks/task_medium.py +9 -8
baseline/__pycache__/policy.cpython-313.pyc
CHANGED
|
Binary files a/baseline/__pycache__/policy.cpython-313.pyc and b/baseline/__pycache__/policy.cpython-313.pyc differ
|
|
|
epidemic_containment_env.zip
ADDED
|
Binary file (90.9 kB). View file
|
|
|
server/constants.py
CHANGED
|
@@ -1,78 +1,44 @@
|
|
| 1 |
# constants.py
|
| 2 |
-
|
| 3 |
-
# Single source of truth for all numeric configuration in the environment.
|
| 4 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 5 |
-
|
| 6 |
-
import sys
|
| 7 |
-
import os
|
| 8 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
| 9 |
|
| 10 |
-
# ββ Task Configuration ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11 |
-
|
| 12 |
TASK_CONFIG = {
|
| 13 |
-
"easy":
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
"resource_pool": 10,
|
| 17 |
-
"data_lag_days": 0,
|
| 18 |
-
},
|
| 19 |
-
"medium": {
|
| 20 |
-
"num_districts": 4,
|
| 21 |
-
"max_steps": 15,
|
| 22 |
-
"resource_pool": 8,
|
| 23 |
-
"data_lag_days": 0,
|
| 24 |
-
},
|
| 25 |
-
"hard": {
|
| 26 |
-
"num_districts": 6,
|
| 27 |
-
"max_steps": 15,
|
| 28 |
-
"resource_pool": 7,
|
| 29 |
-
"data_lag_days": 3,
|
| 30 |
-
},
|
| 31 |
}
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
INFECTION_THRESHOLD = 0.40 # Above this β district is in danger
|
| 37 |
-
SAFE_THRESHOLD = 0.20 # Below this β district is contained
|
| 38 |
-
LOW_THRESHOLD = 0.20 # Below this β restriction is unnecessary
|
| 39 |
-
|
| 40 |
# Real health systems reach operational collapse before zero capacity.
|
| 41 |
-
# ICU overflow
|
| 42 |
-
HOSPITAL_BREACH_POINT = 0.10
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
# ββ Spread Mechanics ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
|
| 47 |
SPREAD_RATE_MIN = 0.03
|
| 48 |
SPREAD_RATE_MAX = 0.08
|
| 49 |
GROWTH_HINT_NOISE = 0.03
|
| 50 |
|
| 51 |
-
# Natural recovery: ~
|
| 52 |
-
# Reflects a realistic
|
| 53 |
-
# Infection grows without intervention but
|
| 54 |
-
|
| 55 |
-
NATURAL_RECOVERY_RATE = 0.02
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
RESTRICT_REDUCTION = 0.05 # Restricting reduces spread per step
|
| 60 |
-
SPILLOVER_RATE = 0.01 # Infection
|
| 61 |
|
| 62 |
RESOURCE_REPLENISH = 1 # Resource units restored each step (capped at pool)
|
| 63 |
|
| 64 |
-
|
| 65 |
-
# ββ Reward Weights ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 66 |
-
|
| 67 |
REWARD_INFECTION_PENALTY = -0.50
|
| 68 |
REWARD_HOSPITAL_BREACH = -1.00
|
| 69 |
REWARD_EARLY_CONTAINMENT = +0.50
|
| 70 |
REWARD_UNNECESSARY_RESTRICTION = -0.20
|
| 71 |
REWARD_CORRECT_PRIORITISATION = +0.30
|
| 72 |
|
| 73 |
-
|
| 74 |
-
# ββ Episode Terminal Conditions βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 75 |
-
|
| 76 |
# Success: ALL districts below SAFE_THRESHOLD β speed bonus fires
|
| 77 |
# Failure: ANY hospital at or below HOSPITAL_BREACH_POINT β episode ends
|
| 78 |
# Natural: max_steps reached
|
|
|
|
| 1 |
# constants.py
|
| 2 |
+
import sys, os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
| 4 |
|
|
|
|
|
|
|
| 5 |
TASK_CONFIG = {
|
| 6 |
+
"easy": {"num_districts": 2, "max_steps": 10, "resource_pool": 10, "data_lag_days": 0},
|
| 7 |
+
"medium": {"num_districts": 4, "max_steps": 15, "resource_pool": 8, "data_lag_days": 0},
|
| 8 |
+
"hard": {"num_districts": 6, "max_steps": 15, "resource_pool": 7, "data_lag_days": 3},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
+
INFECTION_THRESHOLD = 0.40
|
| 12 |
+
SAFE_THRESHOLD = 0.20
|
| 13 |
+
LOW_THRESHOLD = 0.20
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
# Real health systems reach operational collapse before zero capacity.
|
| 15 |
+
# ICU overflow and triage failure begin at ~10% remaining capacity.
|
| 16 |
+
HOSPITAL_BREACH_POINT = 0.10
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
SPREAD_RATE_MIN = 0.03
|
| 19 |
SPREAD_RATE_MAX = 0.08
|
| 20 |
GROWTH_HINT_NOISE = 0.03
|
| 21 |
|
| 22 |
+
# Natural recovery: ~1% of active cases resolve per day without intervention.
|
| 23 |
+
# Reflects a realistic epidemic where spread dominates unless actively managed.
|
| 24 |
+
# Infection grows without intervention, but sustained allocation can drive it below threshold.
|
| 25 |
+
NATURAL_RECOVERY_RATE = 0.01
|
|
|
|
| 26 |
|
| 27 |
+
# Treatment reduces existing infection by 5% per allocation action.
|
| 28 |
+
# Medical deployment (antivirals, PPE, rapid response teams) realistic at this scale.
|
| 29 |
+
TREATMENT_REDUCTION = 0.05
|
| 30 |
+
ALLOCATE_REDUCTION = 0.10 # Reduces future spread rate this step
|
| 31 |
RESTRICT_REDUCTION = 0.05 # Restricting reduces spread per step
|
| 32 |
+
SPILLOVER_RATE = 0.01 # Infection spilling to adjacent districts
|
| 33 |
|
| 34 |
RESOURCE_REPLENISH = 1 # Resource units restored each step (capped at pool)
|
| 35 |
|
|
|
|
|
|
|
|
|
|
| 36 |
REWARD_INFECTION_PENALTY = -0.50
|
| 37 |
REWARD_HOSPITAL_BREACH = -1.00
|
| 38 |
REWARD_EARLY_CONTAINMENT = +0.50
|
| 39 |
REWARD_UNNECESSARY_RESTRICTION = -0.20
|
| 40 |
REWARD_CORRECT_PRIORITISATION = +0.30
|
| 41 |
|
|
|
|
|
|
|
|
|
|
| 42 |
# Success: ALL districts below SAFE_THRESHOLD β speed bonus fires
|
| 43 |
# Failure: ANY hospital at or below HOSPITAL_BREACH_POINT β episode ends
|
| 44 |
# Natural: max_steps reached
|
server/environment.py
CHANGED
|
@@ -27,6 +27,7 @@ from models import (
|
|
| 27 |
ContainmentAction,
|
| 28 |
)
|
| 29 |
from server.constants import (
|
|
|
|
| 30 |
TASK_CONFIG,
|
| 31 |
INFECTION_THRESHOLD,
|
| 32 |
SAFE_THRESHOLD,
|
|
@@ -286,7 +287,7 @@ class EpidemicContainmentEnv(Environment):
|
|
| 286 |
|
| 287 |
# Term 2: Heavy penalty for hospital capacity breach
|
| 288 |
for district in self._city.districts:
|
| 289 |
-
if district.hospital_capacity_remaining <=
|
| 290 |
reward += REWARD_HOSPITAL_BREACH
|
| 291 |
|
| 292 |
# Term 3: Early containment bonus (decays over time)
|
|
|
|
| 27 |
ContainmentAction,
|
| 28 |
)
|
| 29 |
from server.constants import (
|
| 30 |
+
HOSPITAL_BREACH_POINT,
|
| 31 |
TASK_CONFIG,
|
| 32 |
INFECTION_THRESHOLD,
|
| 33 |
SAFE_THRESHOLD,
|
|
|
|
| 287 |
|
| 288 |
# Term 2: Heavy penalty for hospital capacity breach
|
| 289 |
for district in self._city.districts:
|
| 290 |
+
if district.hospital_capacity_remaining <= HOSPITAL_BREACH_POINT:
|
| 291 |
reward += REWARD_HOSPITAL_BREACH
|
| 292 |
|
| 293 |
# Term 3: Early containment bonus (decays over time)
|
server/grader.py
CHANGED
|
@@ -1,190 +1,119 @@
|
|
| 1 |
# server/grader.py
|
| 2 |
-
|
| 3 |
-
# Deterministic scorer for completed Cascade Containment episodes.
|
| 4 |
-
# Called by baseline/evaluator.py after each full episode.
|
| 5 |
-
# Always returns a float in [0.0, 1.0].
|
| 6 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
-
|
| 8 |
-
import sys
|
| 9 |
-
import os
|
| 10 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
| 11 |
|
| 12 |
-
from typing import List
|
| 13 |
from dataclasses import dataclass
|
| 14 |
-
|
| 15 |
from models import CityState, ContainmentAction
|
| 16 |
from server.constants import (
|
| 17 |
INFECTION_THRESHOLD,
|
| 18 |
SAFE_THRESHOLD,
|
| 19 |
HOSPITAL_BREACH_POINT,
|
|
|
|
| 20 |
TASK_CONFIG,
|
| 21 |
)
|
| 22 |
|
| 23 |
|
| 24 |
-
# ββ Trajectory Record βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
-
|
| 26 |
@dataclass
|
| 27 |
class TrajectoryStep:
|
| 28 |
-
"""
|
| 29 |
-
A single recorded step in an episode.
|
| 30 |
-
Stored by environment.py and passed to the grader after episode ends.
|
| 31 |
-
"""
|
| 32 |
step: int
|
| 33 |
-
city_state: CityState
|
| 34 |
-
action: ContainmentAction
|
| 35 |
-
reward: float
|
| 36 |
-
done: bool
|
| 37 |
-
|
| 38 |
|
| 39 |
-
# ββ Grader Score Breakdown ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
|
| 41 |
@dataclass
|
| 42 |
class GradeResult:
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
total_steps: int # Steps taken before episode ended
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# ββ Main Grader βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
-
|
| 59 |
-
def grade_trajectory(
|
| 60 |
-
trajectory: List[TrajectoryStep],
|
| 61 |
-
task_name: str,
|
| 62 |
-
) -> GradeResult:
|
| 63 |
-
"""
|
| 64 |
-
Score a completed episode trajectory.
|
| 65 |
-
|
| 66 |
-
Args:
|
| 67 |
-
trajectory: Ordered list of TrajectoryStep from one full episode.
|
| 68 |
-
task_name: "easy", "medium", or "hard" β affects scoring strictness.
|
| 69 |
-
|
| 70 |
-
Returns:
|
| 71 |
-
GradeResult with final_score in [0.0, 1.0] and full breakdown.
|
| 72 |
-
"""
|
| 73 |
if not trajectory:
|
| 74 |
-
return GradeResult(
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
hospital_score = 0.0,
|
| 78 |
-
efficiency_score = 0.0,
|
| 79 |
-
speed_score = 0.0,
|
| 80 |
-
hospital_breached = False,
|
| 81 |
-
districts_contained = 0,
|
| 82 |
-
total_steps = 0,
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
config = TASK_CONFIG[task_name]
|
| 86 |
num_districts = config["num_districts"]
|
| 87 |
max_steps = config["max_steps"]
|
| 88 |
total_steps = len(trajectory)
|
| 89 |
|
| 90 |
# ββ Component 1: Containment Score βββββββββββββββββββββββββββββββββββββββ
|
| 91 |
# Fraction of district-days that stayed below infection threshold.
|
| 92 |
-
#
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
safe_district_days = 0
|
| 96 |
-
|
| 97 |
-
for step in trajectory[2:]: # skip first 2 steps
|
| 98 |
for district in step.city_state.districts:
|
| 99 |
if district.true_infection_rate <= INFECTION_THRESHOLD:
|
| 100 |
safe_district_days += 1
|
| 101 |
-
|
| 102 |
total_district_days = max(len(trajectory) - 2, 1) * num_districts
|
| 103 |
-
|
| 104 |
-
containment_score = safe_district_days / total_district_days
|
| 105 |
|
| 106 |
# ββ Component 2: Hospital Score βββββββββββββββββββββββββββββββββββββββββββ
|
| 107 |
-
#
|
| 108 |
-
# Any breach = heavy penalty. Near-breach is also penalised proportionally.
|
| 109 |
-
|
| 110 |
hospital_breached = False
|
| 111 |
total_capacity_preserved = 0.0
|
| 112 |
-
|
| 113 |
for step in trajectory:
|
| 114 |
for district in step.city_state.districts:
|
| 115 |
if district.hospital_capacity_remaining <= HOSPITAL_BREACH_POINT:
|
| 116 |
hospital_breached = True
|
| 117 |
total_capacity_preserved += district.hospital_capacity_remaining
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
hospital_score = avg_capacity * (0.6 if hospital_breached else 1.0)
|
| 122 |
-
hospital_score = round(min(1.0, max(0.0, hospital_score)), 4)
|
| 123 |
|
| 124 |
# ββ Component 3: Efficiency Score ββββββββββββββββββββββββββββββββββββββββ
|
| 125 |
-
# Fraction of
|
| 126 |
-
#
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
if
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
if pre_treatment_estimate > INFECTION_THRESHOLD:
|
| 150 |
-
correct_actions += 1
|
| 151 |
-
elif step.action.district_id == highest_id:
|
| 152 |
-
correct_actions += 1
|
| 153 |
-
efficiency_score = correct_actions / len(resource_actions)
|
| 154 |
-
else:
|
| 155 |
-
efficiency_score = 0.5
|
| 156 |
|
| 157 |
# ββ Component 4: Speed Score ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 158 |
-
#
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
last_step = trajectory[-1]
|
| 163 |
-
if last_step.done and total_steps < max_steps:
|
| 164 |
-
speed_score = round(1.0 - (total_steps / max_steps), 4)
|
| 165 |
-
speed_score = max(0.0, speed_score)
|
| 166 |
-
else:
|
| 167 |
-
speed_score = 0.0 # No speed bonus for failed or incomplete episodes
|
| 168 |
|
| 169 |
# ββ Final Weighted Score ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 170 |
-
#
|
| 171 |
-
#
|
| 172 |
-
#
|
| 173 |
-
#
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
speed_score * 0.10
|
| 181 |
-
)
|
| 182 |
-
final_score = round(min(1.0, max(0.0, final_score)), 4)
|
| 183 |
|
| 184 |
-
# ββ Final district count ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 185 |
-
final_step = trajectory[-1]
|
| 186 |
districts_contained = sum(
|
| 187 |
-
1 for d in
|
| 188 |
if d.true_infection_rate < SAFE_THRESHOLD
|
| 189 |
)
|
| 190 |
|
|
@@ -200,15 +129,5 @@ def grade_trajectory(
|
|
| 200 |
)
|
| 201 |
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
def grade_task(
|
| 206 |
-
trajectory: List[TrajectoryStep],
|
| 207 |
-
task_name: str,
|
| 208 |
-
) -> float:
|
| 209 |
-
"""
|
| 210 |
-
Thin wrapper that returns just the final_score float.
|
| 211 |
-
Used by baseline/evaluator.py for clean score reporting.
|
| 212 |
-
"""
|
| 213 |
-
result = grade_trajectory(trajectory, task_name)
|
| 214 |
-
return result.final_score
|
|
|
|
| 1 |
# server/grader.py
|
| 2 |
+
import sys, os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
| 4 |
|
| 5 |
+
from typing import List
|
| 6 |
from dataclasses import dataclass
|
|
|
|
| 7 |
from models import CityState, ContainmentAction
|
| 8 |
from server.constants import (
|
| 9 |
INFECTION_THRESHOLD,
|
| 10 |
SAFE_THRESHOLD,
|
| 11 |
HOSPITAL_BREACH_POINT,
|
| 12 |
+
TREATMENT_REDUCTION,
|
| 13 |
TASK_CONFIG,
|
| 14 |
)
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
| 17 |
@dataclass
|
| 18 |
class TrajectoryStep:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
step: int
|
| 20 |
+
city_state: CityState
|
| 21 |
+
action: ContainmentAction
|
| 22 |
+
reward: float
|
| 23 |
+
done: bool
|
|
|
|
| 24 |
|
|
|
|
| 25 |
|
| 26 |
@dataclass
|
| 27 |
class GradeResult:
|
| 28 |
+
final_score: float
|
| 29 |
+
containment_score: float
|
| 30 |
+
hospital_score: float
|
| 31 |
+
efficiency_score: float
|
| 32 |
+
speed_score: float
|
| 33 |
+
hospital_breached: bool
|
| 34 |
+
districts_contained: int
|
| 35 |
+
total_steps: int
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def grade_trajectory(trajectory: List[TrajectoryStep], task_name: str) -> GradeResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
if not trajectory:
|
| 40 |
+
return GradeResult(0.0, 0.0, 0.0, 0.0, 0.0, False, 0, 0)
|
| 41 |
+
|
| 42 |
+
config = TASK_CONFIG[task_name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
num_districts = config["num_districts"]
|
| 44 |
max_steps = config["max_steps"]
|
| 45 |
total_steps = len(trajectory)
|
| 46 |
|
| 47 |
# ββ Component 1: Containment Score βββββββββββββββββββββββββββββββββββββββ
|
| 48 |
# Fraction of district-days that stayed below infection threshold.
|
| 49 |
+
# Skips first 2 steps (initial conditions outside agent's control).
|
| 50 |
+
safe_district_days = 0
|
| 51 |
+
for step in trajectory[2:]:
|
|
|
|
|
|
|
|
|
|
| 52 |
for district in step.city_state.districts:
|
| 53 |
if district.true_infection_rate <= INFECTION_THRESHOLD:
|
| 54 |
safe_district_days += 1
|
|
|
|
| 55 |
total_district_days = max(len(trajectory) - 2, 1) * num_districts
|
| 56 |
+
containment_score = safe_district_days / total_district_days
|
|
|
|
| 57 |
|
| 58 |
# ββ Component 2: Hospital Score βββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
+
# Average capacity preserved. Breach multiplier of 0.6 if any district collapsed.
|
|
|
|
|
|
|
| 60 |
hospital_breached = False
|
| 61 |
total_capacity_preserved = 0.0
|
|
|
|
| 62 |
for step in trajectory:
|
| 63 |
for district in step.city_state.districts:
|
| 64 |
if district.hospital_capacity_remaining <= HOSPITAL_BREACH_POINT:
|
| 65 |
hospital_breached = True
|
| 66 |
total_capacity_preserved += district.hospital_capacity_remaining
|
| 67 |
+
hospital_district_days = total_steps * num_districts
|
| 68 |
+
avg_capacity = total_capacity_preserved / hospital_district_days
|
| 69 |
+
hospital_score = round(min(1.0, max(0.0, avg_capacity * (0.6 if hospital_breached else 1.0))), 4)
|
|
|
|
|
|
|
| 70 |
|
| 71 |
# ββ Component 3: Efficiency Score ββββββββββββββββββββββββββββββββββββββββ
|
| 72 |
+
# Fraction of resource actions that targeted the right district.
|
| 73 |
+
# Uses the PREVIOUS step's infection rates (pre-action state) so that
|
| 74 |
+
# successful treatments are not penalised retroactively.
|
| 75 |
+
correct_actions = 0
|
| 76 |
+
total_resource = 0
|
| 77 |
+
for idx, step in enumerate(trajectory):
|
| 78 |
+
if step.action.action_type not in {"allocate", "test"}:
|
| 79 |
+
continue
|
| 80 |
+
total_resource += 1
|
| 81 |
+
# Determine pre-action infection state
|
| 82 |
+
if idx > 0:
|
| 83 |
+
prev_districts = trajectory[idx - 1].city_state.districts
|
| 84 |
+
pre_action_rate = prev_districts[step.action.district_id].true_infection_rate
|
| 85 |
+
highest_before = max(prev_districts, key=lambda d: d.true_infection_rate).district_id
|
| 86 |
+
else:
|
| 87 |
+
# First step: estimate pre-action rate from post-action + treatment
|
| 88 |
+
curr_d = step.city_state.districts[step.action.district_id]
|
| 89 |
+
pre_action_rate = curr_d.true_infection_rate + TREATMENT_REDUCTION
|
| 90 |
+
highest_before = max(step.city_state.districts, key=lambda d: d.true_infection_rate).district_id
|
| 91 |
+
# Correct if the targeted district was above threshold before action,
|
| 92 |
+
# or if it was the most infected district at the time
|
| 93 |
+
if pre_action_rate > INFECTION_THRESHOLD or step.action.district_id == highest_before:
|
| 94 |
+
correct_actions += 1
|
| 95 |
+
efficiency_score = correct_actions / max(total_resource, 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
# ββ Component 4: Speed Score ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 98 |
+
# Reward early containment. Only fires if episode ended before max_steps.
|
| 99 |
+
last_step = trajectory[-1]
|
| 100 |
+
speed_score = round(max(0.0, 1.0 - total_steps / max_steps), 4) \
|
| 101 |
+
if last_step.done and total_steps < max_steps else 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
# ββ Final Weighted Score ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 104 |
+
# Hospital (45%) is the primary constraint β system collapse is catastrophic.
|
| 105 |
+
# Containment (30%) β keeping infection below dangerous levels.
|
| 106 |
+
# Efficiency (15%) β quality of resource allocation decisions.
|
| 107 |
+
# Speed (10%) β tiebreaker rewarding proactive early containment.
|
| 108 |
+
final_score = round(min(1.0, max(0.0,
|
| 109 |
+
containment_score * 0.30 +
|
| 110 |
+
hospital_score * 0.45 +
|
| 111 |
+
efficiency_score * 0.15 +
|
| 112 |
+
speed_score * 0.10
|
| 113 |
+
)), 4)
|
|
|
|
|
|
|
|
|
|
| 114 |
|
|
|
|
|
|
|
| 115 |
districts_contained = sum(
|
| 116 |
+
1 for d in trajectory[-1].city_state.districts
|
| 117 |
if d.true_infection_rate < SAFE_THRESHOLD
|
| 118 |
)
|
| 119 |
|
|
|
|
| 129 |
)
|
| 130 |
|
| 131 |
|
| 132 |
+
def grade_task(trajectory: List[TrajectoryStep], task_name: str) -> float:
|
| 133 |
+
return grade_trajectory(trajectory, task_name).final_score
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/tasks/task_easy.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 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 |
|
|
@@ -11,7 +10,6 @@ from server.constants import TASK_CONFIG
|
|
| 11 |
|
| 12 |
|
| 13 |
class EasyTask(BaseTask):
|
| 14 |
-
|
| 15 |
name = "easy"
|
| 16 |
num_districts = TASK_CONFIG["easy"]["num_districts"]
|
| 17 |
max_steps = TASK_CONFIG["easy"]["max_steps"]
|
|
@@ -19,10 +17,12 @@ class EasyTask(BaseTask):
|
|
| 19 |
data_lag_days = TASK_CONFIG["easy"]["data_lag_days"]
|
| 20 |
|
| 21 |
def build_initial_state(self) -> CityState:
|
| 22 |
-
# D0
|
| 23 |
-
#
|
| 24 |
-
#
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
|
| 27 |
return CityState(
|
| 28 |
day = 0,
|
|
|
|
| 1 |
# server/tasks/task_easy.py
|
| 2 |
+
import sys, os
|
|
|
|
| 3 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
| 4 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
|
| 5 |
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class EasyTask(BaseTask):
|
|
|
|
| 13 |
name = "easy"
|
| 14 |
num_districts = TASK_CONFIG["easy"]["num_districts"]
|
| 15 |
max_steps = TASK_CONFIG["easy"]["max_steps"]
|
|
|
|
| 17 |
data_lag_days = TASK_CONFIG["easy"]["data_lag_days"]
|
| 18 |
|
| 19 |
def build_initial_state(self) -> CityState:
|
| 20 |
+
# D0 starts at the danger threshold β one district with a visible outbreak.
|
| 21 |
+
# D1 is very clean, grows slowly through spillover only.
|
| 22 |
+
# With TREATMENT_REDUCTION=0.05 and good strategy, agent contains both
|
| 23 |
+
# districts in 6-8 steps, earning a speed bonus. Requires sustained focus
|
| 24 |
+
# on D0 first before D1 grows above safe threshold.
|
| 25 |
+
seed_infections = [0.40, 0.04]
|
| 26 |
|
| 27 |
return CityState(
|
| 28 |
day = 0,
|
server/tasks/task_medium.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
# server/tasks/task_medium.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 |
|
|
@@ -11,7 +10,6 @@ from server.constants import TASK_CONFIG
|
|
| 11 |
|
| 12 |
|
| 13 |
class MediumTask(BaseTask):
|
| 14 |
-
|
| 15 |
name = "medium"
|
| 16 |
num_districts = TASK_CONFIG["medium"]["num_districts"]
|
| 17 |
max_steps = TASK_CONFIG["medium"]["max_steps"]
|
|
@@ -19,11 +17,14 @@ class MediumTask(BaseTask):
|
|
| 19 |
data_lag_days = TASK_CONFIG["medium"]["data_lag_days"]
|
| 20 |
|
| 21 |
def build_initial_state(self) -> CityState:
|
| 22 |
-
# D0 and D2 seeded with outbreaks (non-adjacent).
|
| 23 |
-
# D1 and D3 start
|
| 24 |
-
#
|
| 25 |
-
#
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
return CityState(
|
| 29 |
day = 0,
|
|
|
|
| 1 |
# server/tasks/task_medium.py
|
| 2 |
+
import sys, os
|
|
|
|
| 3 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
| 4 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
|
| 5 |
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class MediumTask(BaseTask):
|
|
|
|
| 13 |
name = "medium"
|
| 14 |
num_districts = TASK_CONFIG["medium"]["num_districts"]
|
| 15 |
max_steps = TASK_CONFIG["medium"]["max_steps"]
|
|
|
|
| 17 |
data_lag_days = TASK_CONFIG["medium"]["data_lag_days"]
|
| 18 |
|
| 19 |
def build_initial_state(self) -> CityState:
|
| 20 |
+
# D0 and D2 seeded with active outbreaks (non-adjacent boroughs).
|
| 21 |
+
# D1 and D3 start with low infections that grow into danger within
|
| 22 |
+
# 4-6 steps through spillover and their own spread rates.
|
| 23 |
+
# With only 8 resources for 4 districts over 15 steps, the agent
|
| 24 |
+
# cannot contain all districts simultaneously β genuine triage required.
|
| 25 |
+
# A dumb agent scores ~0.35, a smart agent with good prioritisation
|
| 26 |
+
# scores 0.60-0.70.
|
| 27 |
+
seed_infections = [0.35, 0.08, 0.32, 0.08]
|
| 28 |
|
| 29 |
return CityState(
|
| 30 |
day = 0,
|