RohitChandramouli6618 commited on
Commit
ec747a2
Β·
1 Parent(s): bf35524

Fix resource replenishment, grader efficiency, seed calibration, prompt strategy

Browse files
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
- Convert a CityObservation into a clear, structured prompt.
36
- The prompt gives the LLM everything it needs to make an informed decision.
37
- """
 
 
 
 
 
38
  lines = [
39
- "You are a public health authority managing an epidemic outbreak.",
40
- "Your goal is to contain infection across all districts before hospitals collapse.",
41
  "",
42
- f"Current situation (Step {obs.current_step}/{obs.max_steps}):",
43
- f"Available resources: {obs.available_resources}",
44
  "",
45
- "District status:",
46
  ]
47
 
48
- for d in obs.districts:
49
- status = "DANGER" if d.reported_infection_rate > 0.4 else \
50
- "WARNING" if d.reported_infection_rate > 0.2 else "SAFE"
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  lines.append(
52
- f" District {d.district_id}: "
53
- f"infection={d.reported_infection_rate:.2f} [{status}], "
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
- "Available actions:",
63
- " - 'test' : Get accurate infection data for a district (costs 1 resource)",
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.2, # Low temperature for consistent, reliable decisions
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
- Main entry point for the policy.
148
- Takes an observation, returns a ContainmentAction.
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, # Resources available per episode
19
- "data_lag_days": 0, # Agent sees real-time infection data
20
  },
21
  "medium": {
22
  "num_districts": 4,
23
  "max_steps": 15,
24
- "resource_pool": 8, # Tighter budget forces real tradeoffs
25
  "data_lag_days": 0,
26
  },
27
  "hard": {
28
  "num_districts": 6,
29
  "max_steps": 15,
30
- "resource_pool": 7, # Scarce resources + delayed data
31
- "data_lag_days": 3, # Agent sees infection rates from 3 days ago
32
  },
33
  }
34
 
35
 
36
  # ── Infection Thresholds ──────────────────────────────────────────────────────
37
 
38
- INFECTION_THRESHOLD = 0.40 # Above this β†’ district is in danger (penalty fires)
39
- SAFE_THRESHOLD = 0.20 # Below this β†’ district is contained (bonus fires)
40
- LOW_THRESHOLD = 0.20 # Below this β†’ restriction is deemed unnecessary
41
- HOSPITAL_BREACH_POINT = 0.00 # At or below this β†’ hospital has collapsed
42
 
43
 
44
  # ── Spread Mechanics ──────────────────────────────────────────────────────────
45
 
46
- SPREAD_RATE_MIN = 0.04 # Slowest possible true spread rate per day
47
- SPREAD_RATE_MAX = 0.15 # Fastest possible true spread rate per day
48
- GROWTH_HINT_NOISE = 0.03 # Random noise added to growth_rate_hint (Β± value)
49
- TREATMENT_REDUCTION = 0.06 # Allocating also reduces existing infection
50
- ALLOCATE_REDUCTION = 0.10 # How much one 'allocate' reduces spread this step
51
- RESTRICT_REDUCTION = 0.05 # How much one 'restrict' reduces spread per step
52
- SPILLOVER_RATE = 0.01 # Fraction of infection that spreads to adjacent districts per day
 
53
 
54
- RESOURCE_REPLENISH = 3 # Resource units restored at the start of each new day
55
 
56
 
57
  # ── Reward Weights ────────────────────────────────────────────────────────────
58
 
59
- REWARD_INFECTION_PENALTY = -0.50 # Per district above INFECTION_THRESHOLD each step
60
- REWARD_HOSPITAL_BREACH = -1.00 # Per district with breached hospital capacity
61
- REWARD_EARLY_CONTAINMENT = +0.50 # Base value; scaled by (1 - step/max_steps)
62
- REWARD_UNNECESSARY_RESTRICTION = -0.20 # Restricting a district below LOW_THRESHOLD
63
- REWARD_CORRECT_PRIORITISATION = +0.30 # Allocating to the highest-infected district
64
 
65
 
66
  # ── Episode Terminal Conditions ───────────────────────────────────────────────
67
 
68
- # Episode ends early (success) if ALL districts drop below SAFE_THRESHOLD.
69
- # Episode ends early (failure) if ANY district's hospital capacity hits HOSPITAL_BREACH_POINT.
70
- # Otherwise episode runs until max_steps is reached.
 
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
- resource_actions = [
 
 
 
 
 
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
- target = step.city_state.districts[step.action.district_id]
137
- if target.true_infection_rate > INFECTION_THRESHOLD:
 
 
 
 
 
 
 
138
  correct_actions += 1
139
  efficiency_score = correct_actions / len(resource_actions)
140
  else:
141
- efficiency_score = 0.5 # Neutral if no resource actions taken
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__), '..')) # reaches server/
11
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) # reaches project root
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"] # 2
23
- max_steps = TASK_CONFIG["easy"]["max_steps"] # 10
24
- resource_pool = TASK_CONFIG["easy"]["resource_pool"] # 10
25
- data_lag_days = TASK_CONFIG["easy"]["data_lag_days"] # 0
26
 
27
  def build_initial_state(self) -> CityState:
28
- # District 0 has a visible outbreak. District 1 is clean.
29
- # Agent only needs to identify and respond to one threat.
30
- seed_infections = [0.25, 0.05]
 
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 = self.num_districts,
40
- seed_infections = 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
  )