MediGuard-AI / reward_function.py
Rajveer
clean deploy: all files at root
0c6a12e
Raw History Blame Contribute Delete
5.57 kB
"""
Reward Function for MediGuard-AI
================================
Maps (action Γ— patient_condition Γ— activity) to a reward signal with:
- Base reward table
- Activity context multipliers (the novel mechanic)
- Alarm fatigue modifier
- Personalization bonus
"""
from enum import Enum
class Action(Enum):
IGNORE = 0
VERIFY = 1
ALERT = 2
class PatientCondition(Enum):
STABLE = "stable"
BORDERLINE = "borderline"
EMERGENCY = "emergency"
DRUG_MASKED = "drug_masked"
# Base reward table: REWARD_TABLE[action][condition]
REWARD_TABLE = {
Action.ALERT: {
PatientCondition.EMERGENCY: +1.0,
PatientCondition.BORDERLINE: +0.2,
PatientCondition.STABLE: -0.5,
PatientCondition.DRUG_MASKED: +1.0,
},
Action.VERIFY: {
PatientCondition.EMERGENCY: +0.3,
PatientCondition.BORDERLINE: +0.7,
PatientCondition.STABLE: -0.1,
PatientCondition.DRUG_MASKED: +0.3,
},
Action.IGNORE: {
PatientCondition.EMERGENCY: -1.0,
PatientCondition.BORDERLINE: -0.2,
PatientCondition.STABLE: +0.2,
PatientCondition.DRUG_MASKED: -1.0,
},
}
# Activity context multipliers β€” the key insight:
# Same vital sign means different things depending on what the patient is doing.
# HR 130 while walking β†’ expected (low multiplier, discount the anomaly)
# HR 130 while lying still β†’ emergency (high multiplier, amplify the anomaly)
ACTIVITY_CONTEXT = {
0: 1.00, # resting (lying in bed) β€” baseline, no discount
1: 0.40, # eating β€” slight HR/BP increase expected
2: 0.50, # walking/ambulating β€” elevated vitals expected
3: 1.25, # distressed β€” amplify concern
4: 1.60, # falling β€” immediate concern
}
# Alarm fatigue settings
FATIGUE_WINDOW = 30 # look at last 30 steps
FATIGUE_THRESHOLD = 5 # more than 5 alerts in the window
FATIGUE_MULTIPLIER = 0.6 # reduce reward to 60%
# Personalization settings
PERSONALIZATION_STEP = 200 # kicks in after this many steps
PERSONALIZATION_BONUS = 0.2 # bonus for correctly ignoring known-normal
class RewardFunction:
"""
Stateful reward calculator.
Tracks action history for alarm fatigue and personalization bonuses.
"""
def __init__(self):
self.action_history = []
self.condition_history = []
self.activity_history = []
self.step_count = 0
def reset(self):
"""Clear all history for a new episode."""
self.action_history = []
self.condition_history = []
self.activity_history = []
self.step_count = 0
def compute(self, action: Action, condition: PatientCondition,
activity: int = 0) -> float:
"""
Compute the reward for a single (action, condition, activity) tuple.
Parameters
----------
action : Action
The agent's action (IGNORE, VERIFY, ALERT).
condition : PatientCondition
The patient's current condition.
activity : int
The patient's current activity code (0-4).
Returns
-------
float
The reward value (can be negative).
"""
self.step_count += 1
self.action_history.append(action)
self.condition_history.append(condition)
self.activity_history.append(activity)
# 1. Base reward from the table
base_reward = REWARD_TABLE[action][condition]
# 2. Activity context multiplier
# Only apply to penalty situations β€” don't discount correct actions
ctx = ACTIVITY_CONTEXT.get(activity, 1.0)
if base_reward < 0:
# Penalties are reduced during expected-high-vitals activities
# e.g., alerting during walking gets less penalty (ctx=0.5)
base_reward *= ctx
elif condition in (PatientCondition.EMERGENCY, PatientCondition.DRUG_MASKED):
# Correct emergency responses amplified during dangerous activities
base_reward *= ctx
# 3. Alarm fatigue modifier
fatigue_modifier = 1.0
if len(self.action_history) >= FATIGUE_WINDOW:
recent_alerts = sum(
1 for a in self.action_history[-FATIGUE_WINDOW:]
if a == Action.ALERT
)
if recent_alerts > FATIGUE_THRESHOLD:
fatigue_modifier = FATIGUE_MULTIPLIER
# 4. Personalization bonus
personalization = 0.0
if (self.step_count > PERSONALIZATION_STEP
and action == Action.IGNORE
and condition == PatientCondition.STABLE):
personalization = PERSONALIZATION_BONUS
# Final reward
reward = base_reward * fatigue_modifier + personalization
return reward
def get_stats(self) -> dict:
"""Return episode statistics for graders."""
total_alerts = sum(1 for a in self.action_history if a == Action.ALERT)
total_verifies = sum(1 for a in self.action_history if a == Action.VERIFY)
total_ignores = sum(1 for a in self.action_history if a == Action.IGNORE)
return {
"total_steps": self.step_count,
"total_alerts": total_alerts,
"total_verifies": total_verifies,
"total_ignores": total_ignores,
"action_history": list(self.action_history),
"condition_history": list(self.condition_history),
"activity_history": list(self.activity_history),
}