updated with real triage methodology
This commit is contained in:
parent
9a70435636
commit
7cf5510401
BIN
vitallink/backend/__pycache__/triage_engine.cpython-39.pyc
Normal file
BIN
vitallink/backend/__pycache__/triage_engine.cpython-39.pyc
Normal file
Binary file not shown.
@ -12,6 +12,7 @@ from contextlib import asynccontextmanager
|
||||
import asyncio
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from triage_engine import TriageEngine, VitalSigns, TriageLevel, triage_from_vitals
|
||||
|
||||
# ============================================================================
|
||||
# LIFESPAN MANAGEMENT
|
||||
@ -118,41 +119,40 @@ wristband_details_cache = {}
|
||||
|
||||
|
||||
def calculate_priority_score(patient: Patient) -> float:
|
||||
score = 0.0
|
||||
"""
|
||||
Calculate priority score using centralized triage engine
|
||||
"""
|
||||
|
||||
tier_scores = {"EMERGENCY": 100, "ALERT": 50, "NORMAL": 0}
|
||||
score += tier_scores.get(patient.current_tier, 0)
|
||||
# If no vitals yet, use basic scoring
|
||||
if not patient.last_vitals:
|
||||
severity_scores = {"severe": 50, "moderate": 30, "mild": 20}
|
||||
return severity_scores.get(patient.severity, 20)
|
||||
|
||||
wait_minutes = (datetime.now() - patient.check_in_time).total_seconds() / 60
|
||||
if wait_minutes > 30:
|
||||
score += (wait_minutes - 30) * 0.5
|
||||
elif wait_minutes > 60:
|
||||
score += (wait_minutes - 60) * 1.0
|
||||
# Create VitalSigns object
|
||||
vitals = VitalSigns(
|
||||
heart_rate=patient.last_vitals.get("hr_bpm", 75),
|
||||
spo2=patient.last_vitals.get("spo2", 98),
|
||||
temperature=patient.last_vitals.get("temp_c", 37.0),
|
||||
activity=patient.last_vitals.get("activity", 0.0),
|
||||
)
|
||||
|
||||
severity_scores = {"severe": 20, "moderate": 10, "mild": 5}
|
||||
score += severity_scores.get(patient.severity, 0)
|
||||
# Calculate wait time
|
||||
wait_minutes = int((datetime.now() - patient.check_in_time).total_seconds() / 60)
|
||||
|
||||
if patient.last_vitals:
|
||||
hr = patient.last_vitals.get("hr_bpm", 75)
|
||||
spo2 = patient.last_vitals.get("spo2", 98)
|
||||
temp = patient.last_vitals.get("temp_c", 37.0)
|
||||
# Use triage engine to assess
|
||||
assessment = TriageEngine.assess_patient(
|
||||
vitals=vitals,
|
||||
symptoms=patient.symptoms,
|
||||
severity=patient.severity,
|
||||
age=None, # Add age field to Patient model if needed
|
||||
preexisting=[], # Add preexisting field to Patient model if needed
|
||||
wait_time_minutes=wait_minutes,
|
||||
)
|
||||
|
||||
if hr > 110 or hr < 50:
|
||||
score += 10
|
||||
if hr > 140 or hr < 40:
|
||||
score += 30
|
||||
# Update patient tier based on assessment
|
||||
patient.current_tier = assessment["tier_name"]
|
||||
|
||||
if spo2 < 92:
|
||||
score += 15
|
||||
if spo2 < 88:
|
||||
score += 40
|
||||
|
||||
if temp > 38.5:
|
||||
score += 15
|
||||
if temp > 39.5:
|
||||
score += 25
|
||||
|
||||
return score
|
||||
return assessment["priority_score"]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@ -203,24 +203,57 @@ async def check_in_patient(data: PatientCheckIn):
|
||||
|
||||
@app.post("/api/vitals")
|
||||
async def receive_vitals(data: VitalsData):
|
||||
"""Receive vitals data from base station"""
|
||||
|
||||
patient_id = data.patient_id
|
||||
|
||||
if patient_id not in patients_db:
|
||||
raise HTTPException(status_code=404, detail="Patient not found")
|
||||
|
||||
patient = patients_db[patient_id]
|
||||
patient.current_tier = data.tier
|
||||
|
||||
# Use triage engine to determine tier from vitals
|
||||
vitals = VitalSigns(
|
||||
heart_rate=data.hr_bpm,
|
||||
spo2=data.spo2,
|
||||
temperature=data.temp_c,
|
||||
activity=data.activity,
|
||||
)
|
||||
|
||||
# Quick tier assessment
|
||||
tier = triage_from_vitals(data.hr_bpm, data.spo2, data.temp_c)
|
||||
|
||||
patient.current_tier = tier
|
||||
patient.last_vitals = data.dict()
|
||||
|
||||
# Store in history
|
||||
vitals_history[patient_id].append(data)
|
||||
if len(vitals_history[patient_id]) > 1000:
|
||||
vitals_history[patient_id] = vitals_history[patient_id][-1000:]
|
||||
|
||||
# Check for deterioration if we have history
|
||||
if len(vitals_history[patient_id]) >= 3:
|
||||
previous = [
|
||||
VitalSigns(v.hr_bpm, v.spo2, v.temp_c, v.activity)
|
||||
for v in vitals_history[patient_id][-4:-1]
|
||||
]
|
||||
deterioration = TriageEngine.detect_deterioration(vitals, previous)
|
||||
|
||||
if deterioration["deteriorating"]:
|
||||
print(f"⚠️ DETERIORATION DETECTED: {patient_id}")
|
||||
print(f" Concerns: {', '.join(deterioration['concerns'])}")
|
||||
|
||||
# Broadcast update
|
||||
await broadcast_update(
|
||||
{"type": "vitals_update", "patient_id": patient_id, "vitals": data.dict()}
|
||||
{
|
||||
"type": "vitals_update",
|
||||
"patient_id": patient_id,
|
||||
"vitals": data.dict(),
|
||||
"tier": tier,
|
||||
}
|
||||
)
|
||||
|
||||
return {"status": "received"}
|
||||
return {"status": "received", "tier": tier}
|
||||
|
||||
|
||||
@app.get("/api/queue")
|
||||
|
||||
723
vitallink/backend/triage_engine.py
Normal file
723
vitallink/backend/triage_engine.py
Normal file
@ -0,0 +1,723 @@
|
||||
"""
|
||||
VitalLink Triage Engine
|
||||
Centralized triage assessment algorithm based on hospital emergency standards
|
||||
Combines elements from US ESI, UK Manchester, and Canadian CTAS systems
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from typing import List, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
|
||||
# ============================================================================
|
||||
# TRIAGE LEVELS (Lower number = Higher priority)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TriageLevel(IntEnum):
|
||||
"""
|
||||
Four-tier triage system
|
||||
Based on Emergency Severity Index (ESI) and similar systems
|
||||
"""
|
||||
|
||||
LEVEL_1_EMERGENCY = 1 # Immediate life threat - needs resuscitation
|
||||
LEVEL_2_WARNING = 2 # High risk - deteriorating/unstable
|
||||
LEVEL_3_ALERT = 3 # Urgent - abnormal vitals or moderate risk
|
||||
LEVEL_4_NORMAL = 4 # Standard - stable with minor complaints
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VITAL SIGN THRESHOLDS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class VitalThresholds:
|
||||
"""Clinical thresholds for vital sign assessment with hysteresis"""
|
||||
|
||||
# Heart Rate (BPM) - Upgrade thresholds
|
||||
HR_CRITICAL_LOW = 40
|
||||
HR_CRITICAL_HIGH = 140
|
||||
HR_ABNORMAL_LOW = 50
|
||||
HR_ABNORMAL_HIGH = 110
|
||||
|
||||
# Heart Rate - Downgrade thresholds (with hysteresis gap)
|
||||
HR_CRITICAL_LOW_RECOVERY = 45 # Must go above 45 to leave critical
|
||||
HR_CRITICAL_HIGH_RECOVERY = 135 # Must go below 135 to leave critical
|
||||
HR_ABNORMAL_LOW_RECOVERY = 55
|
||||
HR_ABNORMAL_HIGH_RECOVERY = 105
|
||||
|
||||
# SpO2 (%) - Upgrade thresholds
|
||||
SPO2_CRITICAL = 88
|
||||
SPO2_ABNORMAL = 92
|
||||
|
||||
# SpO2 - Downgrade thresholds
|
||||
SPO2_CRITICAL_RECOVERY = 90 # Must go above 90 to leave critical
|
||||
SPO2_ABNORMAL_RECOVERY = 94 # Must go above 94 to leave abnormal
|
||||
|
||||
# Temperature (°C) - Upgrade thresholds
|
||||
TEMP_CRITICAL_LOW = 35.0
|
||||
TEMP_CRITICAL_HIGH = 39.5
|
||||
TEMP_ABNORMAL_LOW = 35.5
|
||||
TEMP_ABNORMAL_HIGH = 38.3
|
||||
|
||||
# Temperature - Downgrade thresholds
|
||||
TEMP_CRITICAL_LOW_RECOVERY = 35.5
|
||||
TEMP_CRITICAL_HIGH_RECOVERY = 39.0
|
||||
TEMP_ABNORMAL_LOW_RECOVERY = 36.0
|
||||
TEMP_ABNORMAL_HIGH_RECOVERY = 38.0
|
||||
|
||||
# Respiratory Rate (breaths/min)
|
||||
RR_CRITICAL_LOW = 10
|
||||
RR_CRITICAL_HIGH = 30
|
||||
RR_ABNORMAL_LOW = 12
|
||||
RR_ABNORMAL_HIGH = 20
|
||||
|
||||
|
||||
@dataclass
|
||||
class TierStabilityTracker:
|
||||
"""Tracks tier changes to prevent oscillation"""
|
||||
|
||||
current_tier: str = "NORMAL"
|
||||
tier_since: float = 0.0 # Timestamp when tier was set
|
||||
consecutive_readings: int = 0
|
||||
tier_change_count: int = 0
|
||||
|
||||
# Confirmation requirements
|
||||
CONFIRMATION_READINGS_UPGRADE = 3 # Need 3 consecutive readings to upgrade
|
||||
CONFIRMATION_READINGS_DOWNGRADE = 5 # Need 5 consecutive readings to downgrade
|
||||
MIN_TIME_IN_TIER = 30.0 # Minimum 30 seconds before tier can change
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLINICAL KNOWLEDGE BASE
|
||||
# ============================================================================
|
||||
|
||||
CRITICAL_CONDITIONS = [
|
||||
"cardiac arrest",
|
||||
"respiratory arrest",
|
||||
"major trauma",
|
||||
"severe hemorrhage",
|
||||
"stroke",
|
||||
"seizure ongoing",
|
||||
"anaphylaxis",
|
||||
"severe chest pain",
|
||||
"unresponsive",
|
||||
]
|
||||
|
||||
HIGH_RISK_SYMPTOMS = [
|
||||
"chest pain",
|
||||
"difficulty breathing",
|
||||
"altered mental status",
|
||||
"severe headache",
|
||||
"excessive bleeding",
|
||||
"abdominal pain",
|
||||
"head injury",
|
||||
"suspected sepsis",
|
||||
]
|
||||
|
||||
MODERATE_RISK_SYMPTOMS = [
|
||||
"fever",
|
||||
"nausea/vomiting",
|
||||
"dizziness",
|
||||
"moderate pain",
|
||||
"injury/trauma",
|
||||
"infection signs",
|
||||
]
|
||||
|
||||
PREEXISTING_CONDITIONS = [
|
||||
"diabetes",
|
||||
"copd",
|
||||
"asthma",
|
||||
"cancer",
|
||||
"hypertension",
|
||||
"heart disease",
|
||||
"kidney disease",
|
||||
"immunocompromised",
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# VITAL SIGNS DATA CLASS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class VitalSigns:
|
||||
"""Current vital signs for a patient"""
|
||||
|
||||
heart_rate: int
|
||||
spo2: int
|
||||
temperature: float
|
||||
activity: float = 0.0
|
||||
respiratory_rate: Optional[int] = None
|
||||
blood_pressure_systolic: Optional[int] = None
|
||||
consciousness_level: str = "Alert"
|
||||
pain_level: int = 0 # 0-10 scale
|
||||
|
||||
def is_critical(self) -> bool:
|
||||
"""Check if any vital is in critical range"""
|
||||
return (
|
||||
self.heart_rate < VitalThresholds.HR_CRITICAL_LOW
|
||||
or self.heart_rate > VitalThresholds.HR_CRITICAL_HIGH
|
||||
or self.spo2 < VitalThresholds.SPO2_CRITICAL
|
||||
or self.temperature < VitalThresholds.TEMP_CRITICAL_LOW
|
||||
or self.temperature > VitalThresholds.TEMP_CRITICAL_HIGH
|
||||
or (
|
||||
self.respiratory_rate
|
||||
and (
|
||||
self.respiratory_rate < VitalThresholds.RR_CRITICAL_LOW
|
||||
or self.respiratory_rate > VitalThresholds.RR_CRITICAL_HIGH
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def is_abnormal(self) -> bool:
|
||||
"""Check if any vital is abnormal but not critical"""
|
||||
return (
|
||||
self.heart_rate < VitalThresholds.HR_ABNORMAL_LOW
|
||||
or self.heart_rate > VitalThresholds.HR_ABNORMAL_HIGH
|
||||
or self.spo2 < VitalThresholds.SPO2_ABNORMAL
|
||||
or self.temperature < VitalThresholds.TEMP_ABNORMAL_LOW
|
||||
or self.temperature > VitalThresholds.TEMP_ABNORMAL_HIGH
|
||||
)
|
||||
|
||||
def get_abnormalities(self) -> List[str]:
|
||||
"""Get list of abnormal vital signs"""
|
||||
abnormalities = []
|
||||
|
||||
if self.heart_rate < VitalThresholds.HR_CRITICAL_LOW:
|
||||
abnormalities.append(f"Severe Bradycardia ({self.heart_rate} bpm)")
|
||||
elif self.heart_rate > VitalThresholds.HR_CRITICAL_HIGH:
|
||||
abnormalities.append(f"Severe Tachycardia ({self.heart_rate} bpm)")
|
||||
elif self.heart_rate < VitalThresholds.HR_ABNORMAL_LOW:
|
||||
abnormalities.append(f"Bradycardia ({self.heart_rate} bpm)")
|
||||
elif self.heart_rate > VitalThresholds.HR_ABNORMAL_HIGH:
|
||||
abnormalities.append(f"Tachycardia ({self.heart_rate} bpm)")
|
||||
|
||||
if self.spo2 < VitalThresholds.SPO2_CRITICAL:
|
||||
abnormalities.append(f"Critical Hypoxia ({self.spo2}%)")
|
||||
elif self.spo2 < VitalThresholds.SPO2_ABNORMAL:
|
||||
abnormalities.append(f"Hypoxia ({self.spo2}%)")
|
||||
|
||||
if self.temperature > VitalThresholds.TEMP_CRITICAL_HIGH:
|
||||
abnormalities.append(f"High Fever ({self.temperature:.1f}°C)")
|
||||
elif self.temperature > VitalThresholds.TEMP_ABNORMAL_HIGH:
|
||||
abnormalities.append(f"Fever ({self.temperature:.1f}°C)")
|
||||
elif self.temperature < VitalThresholds.TEMP_CRITICAL_LOW:
|
||||
abnormalities.append(f"Hypothermia ({self.temperature:.1f}°C)")
|
||||
|
||||
return abnormalities
|
||||
|
||||
def is_critical_with_hysteresis(self, current_tier: str) -> bool:
|
||||
"""Check if critical, with hysteresis based on current tier"""
|
||||
|
||||
if current_tier == "EMERGENCY":
|
||||
# Already in EMERGENCY - use recovery thresholds (harder to leave)
|
||||
return (
|
||||
self.heart_rate < VitalThresholds.HR_CRITICAL_LOW_RECOVERY
|
||||
or self.heart_rate > VitalThresholds.HR_CRITICAL_HIGH_RECOVERY
|
||||
or self.spo2 < VitalThresholds.SPO2_CRITICAL_RECOVERY
|
||||
or self.temperature < VitalThresholds.TEMP_CRITICAL_LOW_RECOVERY
|
||||
or self.temperature > VitalThresholds.TEMP_CRITICAL_HIGH_RECOVERY
|
||||
)
|
||||
else:
|
||||
# Not in EMERGENCY - use standard thresholds (easier to enter)
|
||||
return self.is_critical()
|
||||
|
||||
def is_abnormal_with_hysteresis(self, current_tier: str) -> bool:
|
||||
"""Check if abnormal, with hysteresis based on current tier"""
|
||||
|
||||
if current_tier in ["ALERT", "EMERGENCY"]:
|
||||
# Already elevated - use recovery thresholds
|
||||
return (
|
||||
self.heart_rate < VitalThresholds.HR_ABNORMAL_LOW_RECOVERY
|
||||
or self.heart_rate > VitalThresholds.HR_ABNORMAL_HIGH_RECOVERY
|
||||
or self.spo2 < VitalThresholds.SPO2_ABNORMAL_RECOVERY
|
||||
or self.temperature < VitalThresholds.TEMP_ABNORMAL_LOW_RECOVERY
|
||||
or self.temperature > VitalThresholds.TEMP_ABNORMAL_HIGH_RECOVERY
|
||||
)
|
||||
else:
|
||||
# Normal tier - use standard thresholds
|
||||
return self.is_abnormal()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TRIAGE ENGINE
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TriageEngine:
|
||||
"""
|
||||
Central triage assessment engine
|
||||
Implements hospital-standard triage algorithms
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def assess_patient(
|
||||
vitals: VitalSigns,
|
||||
symptoms: List[str],
|
||||
severity: str,
|
||||
age: Optional[int] = None,
|
||||
preexisting: List[str] = None,
|
||||
wait_time_minutes: int = 0,
|
||||
) -> Dict:
|
||||
"""
|
||||
Comprehensive triage assessment
|
||||
Returns triage level, priority score, and reasoning
|
||||
"""
|
||||
|
||||
preexisting = preexisting or []
|
||||
|
||||
# Step 1: Check for immediate life threats
|
||||
if TriageEngine._is_life_threatening(vitals, symptoms):
|
||||
return {
|
||||
"triage_level": TriageLevel.LEVEL_1_EMERGENCY,
|
||||
"tier_name": "EMERGENCY",
|
||||
"priority_score": 200.0,
|
||||
"reasoning": "Life-threatening condition detected",
|
||||
"abnormalities": vitals.get_abnormalities(),
|
||||
"recommended_action": "Immediate intervention required",
|
||||
}
|
||||
|
||||
# Step 2: Assess high-risk situations
|
||||
if TriageEngine._is_high_risk(vitals, symptoms, severity, age, preexisting):
|
||||
base_score = 100.0
|
||||
|
||||
# Add points for vital abnormalities
|
||||
if vitals.is_abnormal():
|
||||
base_score += 30.0
|
||||
|
||||
# Add wait time urgency
|
||||
if wait_time_minutes > 30:
|
||||
base_score += (wait_time_minutes - 30) * 0.5
|
||||
|
||||
return {
|
||||
"triage_level": TriageLevel.LEVEL_2_WARNING,
|
||||
"tier_name": "ALERT",
|
||||
"priority_score": base_score,
|
||||
"reasoning": "High-risk patient with concerning symptoms",
|
||||
"abnormalities": vitals.get_abnormalities(),
|
||||
"recommended_action": "Rapid assessment needed",
|
||||
}
|
||||
|
||||
# Step 3: Evaluate moderate urgency
|
||||
resources_needed = TriageEngine._estimate_resources(symptoms, severity)
|
||||
|
||||
if resources_needed >= 2 or vitals.is_abnormal():
|
||||
base_score = 50.0
|
||||
|
||||
if vitals.is_abnormal():
|
||||
base_score += 20.0
|
||||
|
||||
if wait_time_minutes > 45:
|
||||
base_score += (wait_time_minutes - 45) * 0.8
|
||||
|
||||
# Check for preexisting conditions
|
||||
if any(
|
||||
cond.lower() in [p.lower() for p in preexisting]
|
||||
for cond in PREEXISTING_CONDITIONS
|
||||
):
|
||||
base_score += 15.0
|
||||
|
||||
return {
|
||||
"triage_level": TriageLevel.LEVEL_3_ALERT,
|
||||
"tier_name": "ALERT",
|
||||
"priority_score": base_score,
|
||||
"reasoning": "Urgent care needed - abnormal vitals or complex case",
|
||||
"abnormalities": vitals.get_abnormalities(),
|
||||
"recommended_action": "See within 30 minutes",
|
||||
}
|
||||
|
||||
# Step 4: Standard care
|
||||
base_score = 20.0
|
||||
|
||||
# Severity modifier
|
||||
if severity == "severe":
|
||||
base_score += 15.0
|
||||
elif severity == "moderate":
|
||||
base_score += 10.0
|
||||
else:
|
||||
base_score += 5.0
|
||||
|
||||
# Long wait compensation
|
||||
if wait_time_minutes > 60:
|
||||
base_score += (wait_time_minutes - 60) * 1.0
|
||||
|
||||
return {
|
||||
"triage_level": TriageLevel.LEVEL_4_NORMAL,
|
||||
"tier_name": "NORMAL",
|
||||
"priority_score": base_score,
|
||||
"reasoning": "Stable patient - standard care",
|
||||
"abnormalities": [],
|
||||
"recommended_action": "See within 120 minutes",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _is_life_threatening(vitals: VitalSigns, symptoms: List[str]) -> bool:
|
||||
"""Detect immediate life threats"""
|
||||
|
||||
# Critical vital signs
|
||||
if vitals.is_critical():
|
||||
return True
|
||||
|
||||
# Unresponsive patient
|
||||
if vitals.consciousness_level.lower() in ["unresponsive", "unconscious"]:
|
||||
return True
|
||||
|
||||
# Critical conditions in symptoms
|
||||
return any(
|
||||
cond.lower() in " ".join(symptoms).lower() for cond in CRITICAL_CONDITIONS
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_high_risk(
|
||||
vitals: VitalSigns,
|
||||
symptoms: List[str],
|
||||
severity: str,
|
||||
age: Optional[int],
|
||||
preexisting: List[str],
|
||||
) -> bool:
|
||||
"""Detect high-risk patients"""
|
||||
|
||||
# High-risk age groups
|
||||
if age and (age < 1 or age > 65):
|
||||
if vitals.is_abnormal() or severity == "severe":
|
||||
return True
|
||||
|
||||
# High-risk symptoms
|
||||
symptom_text = " ".join(symptoms).lower()
|
||||
if any(symptom.lower() in symptom_text for symptom in HIGH_RISK_SYMPTOMS):
|
||||
return True
|
||||
|
||||
# Severe self-reported severity with abnormal vitals
|
||||
if severity == "severe" and vitals.is_abnormal():
|
||||
return True
|
||||
|
||||
# Preexisting conditions with concerning vitals
|
||||
if preexisting and vitals.is_abnormal():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _estimate_resources(symptoms: List[str], severity: str) -> int:
|
||||
"""
|
||||
Estimate number of resources needed
|
||||
Resources: labs, imaging, IV access, procedures, consultations
|
||||
"""
|
||||
resource_count = 0
|
||||
|
||||
symptom_text = " ".join(symptoms).lower()
|
||||
|
||||
# Likely needs labs
|
||||
if any(s in symptom_text for s in ["fever", "infection", "pain", "bleeding"]):
|
||||
resource_count += 1
|
||||
|
||||
# Likely needs imaging
|
||||
if any(
|
||||
s in symptom_text
|
||||
for s in ["head injury", "chest pain", "abdominal", "trauma"]
|
||||
):
|
||||
resource_count += 1
|
||||
|
||||
# Likely needs IV access
|
||||
if any(
|
||||
s in symptom_text
|
||||
for s in ["difficulty breathing", "severe", "bleeding", "dehydration"]
|
||||
):
|
||||
resource_count += 1
|
||||
|
||||
# Complex intervention likely
|
||||
if severity == "severe":
|
||||
resource_count += 1
|
||||
|
||||
return resource_count
|
||||
|
||||
@staticmethod
|
||||
def calculate_priority_score(
|
||||
triage_level: TriageLevel,
|
||||
vitals: VitalSigns,
|
||||
severity: str,
|
||||
wait_time_minutes: int,
|
||||
preexisting: List[str] = None,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate final priority score for queue ordering
|
||||
Higher score = higher priority
|
||||
"""
|
||||
|
||||
preexisting = preexisting or []
|
||||
|
||||
# Base score from triage level
|
||||
level_scores = {
|
||||
TriageLevel.LEVEL_1_EMERGENCY: 200.0,
|
||||
TriageLevel.LEVEL_2_WARNING: 100.0,
|
||||
TriageLevel.LEVEL_3_ALERT: 50.0,
|
||||
TriageLevel.LEVEL_4_NORMAL: 20.0,
|
||||
}
|
||||
score = level_scores.get(triage_level, 0.0)
|
||||
|
||||
# Vital sign modifiers
|
||||
if vitals.is_critical():
|
||||
score += 50.0
|
||||
elif vitals.is_abnormal():
|
||||
score += 20.0
|
||||
|
||||
# Specific vital concerns
|
||||
if vitals.heart_rate > 140 or vitals.heart_rate < 40:
|
||||
score += 30.0
|
||||
if vitals.spo2 < 88:
|
||||
score += 40.0
|
||||
if vitals.temperature > 39.5:
|
||||
score += 25.0
|
||||
|
||||
# Severity modifier
|
||||
severity_scores = {"severe": 25, "moderate": 15, "mild": 5}
|
||||
score += severity_scores.get(severity, 0)
|
||||
|
||||
# Wait time escalation
|
||||
if wait_time_minutes > 30:
|
||||
score += (wait_time_minutes - 30) * 0.5
|
||||
if wait_time_minutes > 60:
|
||||
score += (wait_time_minutes - 60) * 1.0 # Accelerate
|
||||
if wait_time_minutes > 120:
|
||||
score += (wait_time_minutes - 120) * 2.0 # Major escalation
|
||||
|
||||
# Preexisting conditions
|
||||
if preexisting and triage_level != TriageLevel.LEVEL_1_EMERGENCY:
|
||||
score += 10.0 * len(preexisting)
|
||||
|
||||
return round(score, 1)
|
||||
|
||||
@staticmethod
|
||||
def detect_deterioration(
|
||||
current_vitals: VitalSigns,
|
||||
previous_vitals: List[VitalSigns],
|
||||
time_window_minutes: int = 10,
|
||||
) -> Dict:
|
||||
"""
|
||||
Detect if patient is deteriorating based on vital sign trends
|
||||
Returns trend analysis and recommendation
|
||||
"""
|
||||
|
||||
if len(previous_vitals) < 3:
|
||||
return {
|
||||
"deteriorating": False,
|
||||
"reason": "Insufficient data for trend analysis",
|
||||
}
|
||||
|
||||
# Take last 3 readings
|
||||
recent = previous_vitals[-3:]
|
||||
|
||||
# Calculate trends
|
||||
hr_trend = current_vitals.heart_rate - sum(v.heart_rate for v in recent) / len(
|
||||
recent
|
||||
)
|
||||
spo2_trend = sum(v.spo2 for v in recent) / len(recent) - current_vitals.spo2
|
||||
temp_trend = current_vitals.temperature - sum(
|
||||
v.temperature for v in recent
|
||||
) / len(recent)
|
||||
|
||||
# Detect concerning trends
|
||||
concerns = []
|
||||
|
||||
if hr_trend > 15:
|
||||
concerns.append(f"Increasing HR (+{hr_trend:.0f} bpm)")
|
||||
if spo2_trend > 3:
|
||||
concerns.append(f"Decreasing SpO2 (-{spo2_trend:.0f}%)")
|
||||
if temp_trend > 0.5:
|
||||
concerns.append(f"Rising temperature (+{temp_trend:.1f}°C)")
|
||||
|
||||
# Crossed into abnormal range
|
||||
if current_vitals.is_critical() and not any(v.is_critical() for v in recent):
|
||||
concerns.append("Vitals crossed into CRITICAL range")
|
||||
elif current_vitals.is_abnormal() and not any(v.is_abnormal() for v in recent):
|
||||
concerns.append("Vitals crossed into ABNORMAL range")
|
||||
|
||||
deteriorating = len(concerns) > 0
|
||||
|
||||
return {
|
||||
"deteriorating": deteriorating,
|
||||
"concerns": concerns,
|
||||
"hr_trend": hr_trend,
|
||||
"spo2_trend": spo2_trend,
|
||||
"temp_trend": temp_trend,
|
||||
"recommendation": "Escalate care immediately"
|
||||
if deteriorating
|
||||
else "Continue monitoring",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def should_escalate_tier(
|
||||
current_tier: TriageLevel, vitals: VitalSigns, wait_time_minutes: int
|
||||
) -> tuple[bool, Optional[TriageLevel], str]:
|
||||
"""
|
||||
Determine if patient tier should be escalated
|
||||
Returns (should_escalate, new_tier, reason)
|
||||
"""
|
||||
|
||||
# Always escalate if vitals become critical
|
||||
if vitals.is_critical() and current_tier != TriageLevel.LEVEL_1_EMERGENCY:
|
||||
return (
|
||||
True,
|
||||
TriageLevel.LEVEL_1_EMERGENCY,
|
||||
"Vitals entered critical range",
|
||||
)
|
||||
|
||||
# Escalate NORMAL to ALERT if vitals abnormal
|
||||
if current_tier == TriageLevel.LEVEL_4_NORMAL and vitals.is_abnormal():
|
||||
return (True, TriageLevel.LEVEL_3_ALERT, "Vitals became abnormal")
|
||||
|
||||
# Escalate ALERT to WARNING if vitals worsening
|
||||
if current_tier == TriageLevel.LEVEL_3_ALERT and vitals.is_critical():
|
||||
return (
|
||||
True,
|
||||
TriageLevel.LEVEL_2_WARNING,
|
||||
"Vitals worsening toward critical",
|
||||
)
|
||||
|
||||
# Time-based escalation for long waits
|
||||
if wait_time_minutes > 120 and current_tier == TriageLevel.LEVEL_4_NORMAL:
|
||||
return (True, TriageLevel.LEVEL_3_ALERT, "Excessive wait time (>2 hours)")
|
||||
|
||||
if wait_time_minutes > 180 and current_tier == TriageLevel.LEVEL_3_ALERT:
|
||||
return (True, TriageLevel.LEVEL_2_WARNING, "Critical wait time (>3 hours)")
|
||||
|
||||
return (False, None, "No escalation needed")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CONVENIENCE FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def triage_from_vitals(hr: int, spo2: int, temp: float) -> str:
|
||||
"""
|
||||
Quick triage tier determination from vital signs only
|
||||
Returns: "EMERGENCY", "ALERT", or "NORMAL"
|
||||
"""
|
||||
vitals = VitalSigns(heart_rate=hr, spo2=spo2, temperature=temp)
|
||||
|
||||
if vitals.is_critical():
|
||||
return "EMERGENCY"
|
||||
elif vitals.is_abnormal():
|
||||
return "ALERT"
|
||||
else:
|
||||
return "NORMAL"
|
||||
|
||||
|
||||
def assess_new_patient(
|
||||
hr: int,
|
||||
spo2: int,
|
||||
temp: float,
|
||||
symptoms: List[str],
|
||||
severity: str,
|
||||
age: Optional[int] = None,
|
||||
preexisting: List[str] = None,
|
||||
) -> Dict:
|
||||
"""
|
||||
Complete assessment for new patient
|
||||
Returns full triage result with score and recommendations
|
||||
"""
|
||||
|
||||
vitals = VitalSigns(heart_rate=hr, spo2=spo2, temperature=temp)
|
||||
|
||||
result = TriageEngine.assess_patient(
|
||||
vitals=vitals,
|
||||
symptoms=symptoms,
|
||||
severity=severity,
|
||||
age=age,
|
||||
preexisting=preexisting,
|
||||
wait_time_minutes=0,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def reassess_patient(
|
||||
patient_data: Dict, current_vitals: Dict, wait_time_minutes: int
|
||||
) -> Dict:
|
||||
"""
|
||||
Reassess existing patient with updated vitals and wait time
|
||||
Returns updated triage assessment
|
||||
"""
|
||||
|
||||
vitals = VitalSigns(
|
||||
heart_rate=current_vitals.get("hr_bpm", 75),
|
||||
spo2=current_vitals.get("spo2", 98),
|
||||
temperature=current_vitals.get("temp_c", 37.0),
|
||||
activity=current_vitals.get("activity", 0.0),
|
||||
)
|
||||
|
||||
result = TriageEngine.assess_patient(
|
||||
vitals=vitals,
|
||||
symptoms=patient_data.get("symptoms", []),
|
||||
severity=patient_data.get("severity", "moderate"),
|
||||
age=patient_data.get("age"),
|
||||
preexisting=patient_data.get("preexisting", []),
|
||||
wait_time_minutes=wait_time_minutes,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# EXAMPLE USAGE
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 80)
|
||||
print("VitalLink Triage Engine - Test Cases")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
# Test Case 1: Critical patient
|
||||
print("Test 1: Critical Emergency")
|
||||
result = assess_new_patient(
|
||||
hr=155,
|
||||
spo2=85,
|
||||
temp=40.2,
|
||||
symptoms=["Severe Chest Pain", "Difficulty Breathing"],
|
||||
severity="severe",
|
||||
)
|
||||
print(f" Level: {result['triage_level'].name}")
|
||||
print(f" Tier: {result['tier_name']}")
|
||||
print(f" Score: {result['priority_score']}")
|
||||
print(f" Reason: {result['reasoning']}")
|
||||
print(f" Abnormalities: {result['abnormalities']}")
|
||||
print()
|
||||
|
||||
# Test Case 2: Deteriorating patient
|
||||
print("Test 2: Deteriorating Patient")
|
||||
result = assess_new_patient(
|
||||
hr=118,
|
||||
spo2=93,
|
||||
temp=38.5,
|
||||
symptoms=["Fever", "Difficulty Breathing"],
|
||||
severity="moderate",
|
||||
)
|
||||
print(f" Level: {result['triage_level'].name}")
|
||||
print(f" Tier: {result['tier_name']}")
|
||||
print(f" Score: {result['priority_score']}")
|
||||
print()
|
||||
|
||||
# Test Case 3: Stable patient
|
||||
print("Test 3: Stable Patient")
|
||||
result = assess_new_patient(
|
||||
hr=75, spo2=98, temp=36.9, symptoms=["Minor Pain"], severity="mild"
|
||||
)
|
||||
print(f" Level: {result['triage_level'].name}")
|
||||
print(f" Tier: {result['tier_name']}")
|
||||
print(f" Score: {result['priority_score']}")
|
||||
print()
|
||||
|
||||
# Test Case 4: Long wait escalation
|
||||
print("Test 4: Long Wait Time Escalation")
|
||||
vitals = VitalSigns(heart_rate=78, spo2=97, temperature=37.1)
|
||||
should_escalate, new_tier, reason = TriageEngine.should_escalate_tier(
|
||||
current_tier=TriageLevel.LEVEL_4_NORMAL, vitals=vitals, wait_time_minutes=130
|
||||
)
|
||||
print(f" Should escalate: {should_escalate}")
|
||||
print(f" New tier: {new_tier.name if new_tier else 'N/A'}")
|
||||
print(f" Reason: {reason}")
|
||||
print()
|
||||
|
||||
print("=" * 80)
|
||||
@ -1,4 +0,0 @@
|
||||
200440
|
||||
200455
|
||||
200464
|
||||
200523
|
||||
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
200440
|
||||
@ -3,7 +3,7 @@
|
||||
> vite
|
||||
|
||||
|
||||
VITE v7.1.10 ready in 116 ms
|
||||
VITE v7.1.10 ready in 107 ms
|
||||
|
||||
➜ Local: http://localhost:5173/
|
||||
➜ Network: use --host to expose
|
||||
|
||||
@ -1 +0,0 @@
|
||||
200464
|
||||
@ -1 +0,0 @@
|
||||
200523
|
||||
@ -1 +0,0 @@
|
||||
200455
|
||||
Binary file not shown.
@ -181,18 +181,18 @@ class WristbandSimulator:
|
||||
return int(hr), int(spo2), temp, self.activity_level
|
||||
|
||||
def _determine_tier(self, hr: int, spo2: int, temp: float) -> tuple:
|
||||
"""Determine alert tier based on vitals"""
|
||||
"""Determine flags based on vitals - tier determined by backend"""
|
||||
flags = 0
|
||||
|
||||
# Battery warning
|
||||
if self.battery < 15:
|
||||
flags |= VitalFlags.LOW_BATT
|
||||
|
||||
# Check for critical vitals (EMERGENCY)
|
||||
# Set EMERGENCY flag if critical vitals
|
||||
if hr > 140 or hr < 45 or spo2 < 88 or temp > 39.5 or temp < 35.0:
|
||||
flags |= VitalFlags.EMERGENCY
|
||||
tier = "EMERGENCY"
|
||||
# Check for concerning vitals (ALERT)
|
||||
# Set ALERT flag if concerning vitals
|
||||
elif hr > 110 or hr < 50 or spo2 < 92 or temp > 38.3 or temp < 35.5:
|
||||
flags |= VitalFlags.ALERT
|
||||
tier = "ALERT"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user