File size: 5,570 Bytes
0c6a12e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
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),
        }