File size: 19,910 Bytes
4afc4db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be77d11
4afc4db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be77d11
4afc4db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be77d11
4afc4db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be77d11
4afc4db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
"""
Task configurations for the MedChain Env environment.

Four tasks of increasing difficulty:
  0. orientation_ward        β€” 2 days,  1 ward,  no events       (intro)
  1. single_ward_stable      β€” 3 days,  1 ward,  no events
  2. multi_ward_seasonal     β€” 6 days,  4 wards, flu surge + supplier delay
  3. hospital_network_crisis β€” 12 days, 4 sites, 5 overlapping crises
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional


@dataclass
class Product:
    product_id: str
    name: str
    shelf_life_days: Optional[int]  # None = non-perishable
    criticality: str                # "CRITICAL", "HIGH", "NORMAL"
    unit_cost: float
    locations: List[str]
    base_demand: float
    demand_std: float
    seasonal_amplitude: float       # 0.0 = no seasonality
    seasonal_period: int            # 0 = no cycle
    seasonal_phase: float           # phase offset in radians


@dataclass
class Supplier:
    supplier_id: str
    name: str
    base_lead_time: int
    lead_time_std: float            # 0.0 = deterministic
    cost_multiplier: float
    products: List[str]


@dataclass
class Location:
    location_id: str
    name: str
    capacity: Optional[int]         # None = unlimited


@dataclass(frozen=True)
class InboxMessageTemplate:
    priority: str
    sender: str
    subject: str
    body: str


@dataclass
class SimEvent:
    event_id: str
    event_type: str                 # "mci", "supplier_disruption", "product_recall",
                                    # "demand_surge", "budget_tighten", "cold_chain_breach"
    trigger_day: int
    duration_days: int              # 0 = instant/one-off
    params: Dict
    message: InboxMessageTemplate
    warning_message: Optional[InboxMessageTemplate]  # fires on trigger_day-1


@dataclass
class TaskConfig:
    name: str
    max_days: int
    actions_per_shift: int
    budget_limit: float
    products: List[Product]
    suppliers: List[Supplier]
    locations: List[Location]
    events: List[SimEvent]
    initial_stock_days: int
    benchmark_score: float


# ─── Task 0: Orientation Ward ────────────────────────────────────────────────

TASK0 = TaskConfig(
    name="orientation_ward",
    max_days=2,
    actions_per_shift=8,
    budget_limit=5_000.0,
    initial_stock_days=1,
    benchmark_score=0.88,
    locations=[
        Location("ward_general", "General Ward", capacity=None),
    ],
    products=[
        Product("GLOVE-001", "Surgical Gloves (box)", None, "NORMAL", 5.0, ["ward_general"], 20, 3, 0.0, 0, 0.0),
        Product("SYR-10",    "Syringes 10ml",         None, "NORMAL", 0.5, ["ward_general"], 30, 5, 0.0, 0, 0.0),
        Product("MASK-001",  "Surgical Masks",        None, "NORMAL", 1.0, ["ward_general"], 25, 4, 0.0, 0, 0.0),
    ],
    suppliers=[
        Supplier("MEDLINE", "MedLine Medical", base_lead_time=1, lead_time_std=0.0,
                 cost_multiplier=1.0,
                 products=["GLOVE-001", "SYR-10", "MASK-001"]),
    ],
    events=[],
)


# ─── Task 1: Single Ward Stable ──────────────────────────────────────────────

TASK1 = TaskConfig(
    name="single_ward_stable",
    max_days=3,
    actions_per_shift=10,
    budget_limit=20_000.0,
    initial_stock_days=2,
    benchmark_score=0.68,
    locations=[
        Location("ward_general", "General Ward", capacity=None),
    ],
    products=[
        Product("GLOVE-001", "Surgical Gloves (box)",  None, "NORMAL", 5.0,  ["ward_general"], 50, 8,  0.0, 0, 0.0),
        Product("IV-500",    "IV Bags 500ml",          540,  "NORMAL", 3.5,  ["ward_general"], 30, 5,  0.0, 0, 0.0),
        Product("SYR-10",    "Syringes 10ml",          None, "NORMAL", 0.5,  ["ward_general"], 80, 12, 0.0, 0, 0.0),
        Product("PARA-500",  "Paracetamol 500mg",      360,  "NORMAL", 0.1,  ["ward_general"], 40, 6,  0.0, 0, 0.0),
        Product("MASK-001",  "Surgical Masks",         None, "NORMAL", 1.0,  ["ward_general"], 60, 10, 0.0, 0, 0.0),
        Product("SAL-001",   "Saline Solution",        720,  "NORMAL", 2.5,  ["ward_general"], 25, 4,  0.0, 0, 0.0),
    ],
    suppliers=[
        Supplier("MEDLINE", "MedLine Medical", base_lead_time=2, lead_time_std=0.0,
                 cost_multiplier=1.0,
                 products=["GLOVE-001", "IV-500", "SYR-10", "PARA-500", "MASK-001", "SAL-001"]),
    ],
    events=[],
)


# ─── Task 2: Multi-Ward Seasonal ─────────────────────────────────────────────

TASK2 = TaskConfig(
    name="multi_ward_seasonal",
    max_days=6,
    actions_per_shift=14,
    budget_limit=50_000.0,
    initial_stock_days=3,
    benchmark_score=0.55,
    locations=[
        Location("central_pharmacy", "Central Pharmacy", capacity=15_000),
        Location("ward_icu",         "ICU",              capacity=None),
        Location("ward_emergency",   "Emergency",        capacity=None),
        Location("ward_general",     "General Medicine", capacity=None),
    ],
    products=[
        Product("IV-500",    "IV Bags 500ml",        540,  "NORMAL",   3.5, ["central_pharmacy", "ward_icu"], 25, 4, 0.0, 0, 0.0),
        Product("SAL-001",   "Saline Solution",      720,  "NORMAL",   2.5, ["central_pharmacy", "ward_icu", "ward_emergency"], 20, 3, 0.0, 0, 0.0),
        Product("SYR-10",    "Syringes 10ml",        None, "NORMAL",   0.5, ["central_pharmacy", "ward_icu", "ward_emergency", "ward_general"], 70, 10, 0.0, 0, 0.0),
        Product("ANTIVIR-01","Antiviral Medication",  365, "HIGH",    15.0, ["central_pharmacy", "ward_emergency"], 10, 2, 0.55, 365, 0.0),
        Product("MASK-N95",  "N95 Masks",            None, "NORMAL",   2.5, ["central_pharmacy", "ward_emergency", "ward_icu"], 30, 5, 0.4, 365, 0.0),
        Product("PARA-500",  "Paracetamol 500mg",    360,  "NORMAL",   0.1, ["central_pharmacy", "ward_general"], 40, 6, 0.3, 180, 0.0),
        Product("GLOVE-001", "Surgical Gloves (box)", None, "NORMAL",  5.0, ["central_pharmacy", "ward_icu", "ward_emergency", "ward_general"], 50, 8, 0.0, 0, 0.0),
        Product("SUTURE-01", "Suture Kit",           None, "NORMAL",  12.0, ["central_pharmacy", "ward_general"], 8,  2, 0.0, 0, 0.0),
        Product("DRAIN-01",  "Surgical Drain",       None, "NORMAL",  18.0, ["central_pharmacy", "ward_general"], 5,  1, 0.0, 0, 0.0),
        Product("DRESS-01",  "Wound Dressing Kit",   None, "NORMAL",   6.0, ["central_pharmacy", "ward_general", "ward_emergency"], 20, 4, 0.0, 0, 0.0),
    ],
    suppliers=[
        Supplier("FASTMED", "FastMed Express", base_lead_time=1, lead_time_std=0.0, cost_multiplier=1.4,
                 products=["IV-500", "SAL-001", "SYR-10", "ANTIVIR-01", "MASK-N95", "PARA-500", "GLOVE-001", "SUTURE-01", "DRAIN-01", "DRESS-01"]),
        Supplier("MEDLINE", "MedLine Medical", base_lead_time=4, lead_time_std=0.0, cost_multiplier=1.0,
                 products=["IV-500", "SAL-001", "SYR-10", "ANTIVIR-01", "MASK-N95", "PARA-500", "GLOVE-001", "SUTURE-01", "DRAIN-01", "DRESS-01"]),
    ],
    events=[
        SimEvent(
            event_id="flu_surge",
            event_type="demand_surge",
            trigger_day=3,
            duration_days=3,
            params={"products": ["ANTIVIR-01", "MASK-N95", "PARA-500"], "multiplier": 1.5},
            message=InboxMessageTemplate(
                priority="HIGH",
                sender="Regional Health Authority",
                subject="Influenza Activity Alert β€” Demand Surge Active",
                body=(
                    "Regional influenza activity has been elevated above seasonal baseline.\n"
                    "Emergency department visits up 40-55%. Surge in effect now.\n"
                    "Antiviral, N95, and analgesic stock levels require immediate review.\n"
                    "Prepare for increased ICU admissions over the next 3 days."
                ),
            ),
            warning_message=InboxMessageTemplate(
                priority="LOW",
                sender="Regional Health Authority",
                subject="Early Warning: Influenza Activity Rising",
                body=(
                    "Early indicators suggest influenza activity is rising above seasonal norms.\n"
                    "Consider reviewing antiviral and PPE stock levels as a precaution."
                ),
            ),
        ),
        SimEvent(
            event_id="medline_delay",
            event_type="supplier_disruption",
            trigger_day=4,
            duration_days=3,
            params={"supplier_id": "MEDLINE", "new_lead_time": 7, "reason": "industrial action"},
            message=InboxMessageTemplate(
                priority="HIGH",
                sender="MedLine Medical β€” Supply Chain",
                subject="Service Disruption Notice β€” Lead Time Extension",
                body=(
                    "Due to ongoing industrial action at our primary warehouse facility,\n"
                    "MedLine Medical lead times are currently extended from 4 to 7 days.\n"
                    "This affects all standard orders. FastMed Express remains unaffected.\n"
                    "We apologise for the inconvenience. Reference: SUPDIS-2026-0006."
                ),
            ),
            warning_message=None,
        ),
    ],
)


# ─── Task 3: Hospital Network Crisis ─────────────────────────────────────────

TASK3 = TaskConfig(
    name="hospital_network_crisis",
    max_days=12,
    actions_per_shift=18,
    budget_limit=150_000.0,
    initial_stock_days=4,
    benchmark_score=0.38,
    locations=[
        Location("regional_dc", "Regional Distribution Centre", capacity=None),
        Location("hospital_a",  "Hospital A",                   capacity=None),
        Location("hospital_b",  "Hospital B",                   capacity=None),
        Location("hospital_c",  "Hospital C",                   capacity=None),
    ],
    products=[
        Product("B-001",     "O-Negative Blood (RBC)",      42,   "CRITICAL", 350.0, ["regional_dc", "hospital_a", "hospital_b", "hospital_c"], 3, 1, 0.0, 0, 0.0),
        Product("B-002",     "Platelet Pack",               5,    "CRITICAL", 250.0, ["regional_dc", "hospital_a", "hospital_b", "hospital_c"], 2, 1, 0.0, 0, 0.0),
        Product("FFP-001",   "Fresh Frozen Plasma",         365,  "HIGH",     120.0, ["regional_dc", "hospital_a", "hospital_b", "hospital_c"], 4, 1, 0.0, 0, 0.0),
        Product("INS-001",   "Insulin (opened vial)",       28,   "HIGH",     45.0,  ["hospital_a", "hospital_b", "hospital_c"], 8,  2, 0.0, 0, 0.0),
        Product("CHEMO-01",  "Chemotherapy Agent (recon.)", 7,    "HIGH",     800.0, ["hospital_a", "hospital_b"],              1,  0, 0.0, 0, 0.0),
        Product("IV-SAL-500","IV Saline Solution 500ml",    720,  "NORMAL",   3.5,   ["regional_dc", "hospital_a", "hospital_b", "hospital_c"], 60, 10, 0.0, 0, 0.0),
        Product("IV-500",    "IV Bags 500ml",               540,  "NORMAL",   3.5,   ["regional_dc", "hospital_a", "hospital_b", "hospital_c"], 40, 7,  0.0, 0, 0.0),
        Product("SYR-10",    "Syringes 10ml",               None, "NORMAL",   0.5,   ["regional_dc", "hospital_a", "hospital_b", "hospital_c"], 100,15, 0.0, 0, 0.0),
        Product("GLOVE-001", "Surgical Gloves (box)",       None, "NORMAL",   5.0,   ["regional_dc", "hospital_a", "hospital_b", "hospital_c"], 40, 6,  0.0, 0, 0.0),
        Product("PARA-500",  "Paracetamol 500mg",           360,  "NORMAL",   0.1,   ["hospital_a", "hospital_b", "hospital_c"], 60, 8,  0.0, 0, 0.0),
        Product("B-003",     "AB-Positive Blood (RBC)",     42,   "HIGH",     320.0, ["regional_dc", "hospital_a", "hospital_b", "hospital_c"], 2, 1, 0.0, 0, 0.0),
        Product("MASK-001",  "Surgical Masks",              None, "NORMAL",   1.0,   ["regional_dc", "hospital_a", "hospital_b", "hospital_c"], 80, 12, 0.0, 0, 0.0),
        Product("DRAIN-01",  "Surgical Drain",              None, "NORMAL",   18.0,  ["hospital_a", "hospital_b", "hospital_c"], 4,  1,  0.0, 0, 0.0),
        Product("SAL-001",   "Saline Solution",             720,  "NORMAL",   2.5,   ["regional_dc", "hospital_a", "hospital_b", "hospital_c"], 30, 5,  0.0, 0, 0.0),
        Product("DRESS-01",  "Wound Dressing Kit",          None, "NORMAL",   6.0,   ["hospital_a", "hospital_b", "hospital_c"], 25, 4,  0.0, 0, 0.0),
    ],
    suppliers=[
        Supplier("BLOODBANK-A", "Regional Blood Bank", base_lead_time=1, lead_time_std=0.5, cost_multiplier=1.0,
                 products=["B-001", "B-002", "FFP-001", "B-003"]),
        Supplier("SUPPLIER-A",  "HealthCo Supplies",   base_lead_time=3, lead_time_std=0.5, cost_multiplier=1.0,
                 products=["IV-SAL-500", "IV-500", "SYR-10", "GLOVE-001", "PARA-500", "MASK-001", "DRAIN-01", "SAL-001", "DRESS-01"]),
        Supplier("SUPPLIER-B",  "MedFast Express",     base_lead_time=2, lead_time_std=0.0, cost_multiplier=1.4,
                 products=["IV-SAL-500", "IV-500", "SYR-10", "GLOVE-001", "PARA-500", "MASK-001", "DRAIN-01", "SAL-001", "DRESS-01"]),
        Supplier("PHARMA-X",    "PharmaCorp",          base_lead_time=3, lead_time_std=0.5, cost_multiplier=1.0,
                 products=["INS-001", "CHEMO-01", "PARA-500"]),
    ],
    events=[
        SimEvent(
            event_id="cold_chain_breach",
            event_type="cold_chain_breach",
            trigger_day=3,
            duration_days=0,
            params={
                "location_id": "regional_dc",
                "product_id": "B-002",
                "qty_affected": "all",
            },
            message=InboxMessageTemplate(
                priority="CRITICAL",
                sender="Regional DC Facilities Management",
                subject="URGENT: Cold Chain Breach β€” Platelet Inventory Compromised",
                body=(
                    "Cold chain monitoring detected temperature excursion in Fridge Unit 3.\n"
                    "Recorded +8Β°C for 4 hours (safe range: +20 to +24Β°C).\n"
                    "ALL platelet units at Regional DC are presumed compromised and auto-quarantined.\n"
                    "Affected product: Platelet Pack (B-002) β€” ALL lots at regional_dc.\n"
                    "Action required:\n"
                    "  1. Arrange emergency replacement order from BLOODBANK-A\n"
                    "  2. Notify clinical teams β€” platelet supply now limited to hospital stock only.\n"
                    "Ref: CCBR-2026-0003"
                ),
            ),
            warning_message=None,
        ),
        SimEvent(
            event_id="supplier_a_disruption",
            event_type="supplier_disruption",
            trigger_day=6,
            duration_days=9,
            params={"supplier_id": "SUPPLIER-A", "new_lead_time": 7, "reason": "force majeure β€” flu absenteeism"},
            message=InboxMessageTemplate(
                priority="HIGH",
                sender="HealthCo Supplies β€” Logistics",
                subject="Force Majeure Notice β€” Extended Lead Times",
                body=(
                    "HealthCo Supplies hereby provides formal notice of force majeure event.\n"
                    "Widespread workforce absenteeism due to influenza has impacted fulfilment operations.\n"
                    "Effective immediately, standard lead times are extended from 3 to 7 days.\n"
                    "All product lines affected. MedFast Express (SUPPLIER-B) is unaffected.\n"
                    "Ref: FM-2026-0006"
                ),
            ),
            warning_message=None,
        ),
        SimEvent(
            event_id="mci_warning",
            event_type="demand_surge",
            trigger_day=8,
            duration_days=0,
            params={},
            message=InboxMessageTemplate(
                priority="HIGH",
                sender="Emergency Management Coordination",
                subject="Mass Casualty Incident β€” STANDBY",
                body=(
                    "Multi-vehicle collision reported on Interstate highway.\n"
                    "Current status: STANDBY. Incident Command not yet activated.\n"
                    "Preliminary estimate: 15-25 critically injured.\n"
                    "All blood banks placed on AMBER alert.\n"
                    "Recommend pre-emptive review of O-neg and platelet stock levels NOW.\n"
                    "Further update to follow."
                ),
            ),
            warning_message=None,
        ),
        SimEvent(
            event_id="mci_activation",
            event_type="mci",
            trigger_day=9,
            duration_days=3,
            params={
                "products": ["B-001", "B-002", "FFP-001"],
                "demand_multiplier": 3.0,
                "locations": ["hospital_a", "hospital_b", "hospital_c"],
                "mci_tracking": True,
            },
            message=InboxMessageTemplate(
                priority="CRITICAL",
                sender="Incident Command System",
                subject="MCI ACTIVATION β€” Mass Casualty Event",
                body=(
                    "INCIDENT COMMAND ACTIVATED.\n"
                    "Multi-vehicle collision β€” confirmed 23 critically injured en route.\n"
                    "Hospital A ETA: 18 min | Hospital B ETA: 29 min | Hospital C ETA: 41 min.\n"
                    "Trauma surgery teams mobilised at all hospitals.\n"
                    "All blood banks placed on RED alert.\n"
                    "Est. blood product requirements: 60-90 units O-neg RBC, 30-40 platelet packs.\n"
                    "IMMEDIATE ACTION: Verify blood product inventory and initiate emergency procurement."
                ),
            ),
            warning_message=None,
        ),
        SimEvent(
            event_id="iv_saline_recall",
            event_type="product_recall",
            trigger_day=11,
            duration_days=0,
            params={
                "product_id": "IV-SAL-500",
                "lot_id": "RECALL-LOT-IV2026-9821",
                "locations_with_lot": ["hospital_a", "hospital_b", "hospital_c", "regional_dc"],
                "qty_per_location": 60,
            },
            message=InboxMessageTemplate(
                priority="CRITICAL",
                sender="Pharmacy Automated System",
                subject="MANDATORY RECALL β€” IV Saline Solution 500ml",
                body=(
                    "MANDATORY RECALL β€” Health Authority ref #HA-2026-0013\n"
                    "Product: IV Saline Solution 500ml (IV-SAL-500)\n"
                    "Affected lot: RECALL-LOT-IV2026-9821\n"
                    "Reason: Potential endotoxin contamination detected in batch.\n"
                    "ACTION REQUIRED:\n"
                    "  1. Query inventory at ALL locations for lot RECALL-LOT-IV2026-9821\n"
                    "  2. Quarantine ALL affected units immediately β€” do not use\n"
                    "  3. Submit replacement order\n"
                    "Supplier contact: SUPPLIER-A case #88291. Ref: RECALL-2026-0013."
                ),
            ),
            warning_message=None,
        ),
    ],
)


def get_task_config(task_name: str) -> TaskConfig:
    if task_name == "orientation_ward":
        return TASK0
    elif task_name == "single_ward_stable":
        return TASK1
    elif task_name == "multi_ward_seasonal":
        return TASK2
    elif task_name == "hospital_network_crisis":
        return TASK3
    else:
        raise ValueError(
            f"Unknown task: {task_name!r}. "
            "Choose from: orientation_ward, single_ward_stable, multi_ward_seasonal, hospital_network_crisis"
        )