File size: 2,996 Bytes
ba8004e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
PID controller baseline for blood glucose management.

A simple Proportional-Integral-Derivative controller targeting 120 mg/dL
(centre of the safe 70–180 range). This mirrors the control strategy used
by commercial insulin pumps and serves as a benchmark that RL agents
should eventually outperform.
"""

import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from models import GlucoAction


class PIDController:
    """
    PID controller for insulin dosing.

    Adjusts basal insulin rate proportionally to the error between current
    glucose and target, with integral and derivative terms for steady-state
    accuracy and overshoot damping. Adds a simple correction bolus when
    glucose is very high.

    Args:
        target_glucose: Desired glucose level in mg/dL (default 120).
        kp: Proportional gain.
        ki: Integral gain.
        kd: Derivative gain.
    """

    def __init__(
        self,
        target_glucose: float = 120.0,
        kp: float = 0.02,
        ki: float = 0.0005,
        kd: float = 0.01,
    ):
        self.target = target_glucose
        self.kp = kp
        self.ki = ki
        self.kd = kd
        self.integral = 0.0
        self.prev_error = 0.0

    def reset(self) -> None:
        """Reset the integral and derivative state for a new episode."""
        self.integral = 0.0
        self.prev_error = 0.0

    def act(self, glucose: float) -> GlucoAction:
        """
        Compute insulin action given current glucose reading.

        Args:
            glucose: Current blood glucose in mg/dL.

        Returns:
            GlucoAction with basal_rate and bolus_dose.
        """
        error = glucose - self.target

        # Integral with anti-windup clamping
        self.integral += error
        self.integral = max(-500.0, min(500.0, self.integral))

        # Reset integral when glucose crosses below target to prevent
        # accumulated error from driving dangerous over-delivery
        if glucose < self.target:
            self.integral = min(self.integral, 0.0)

        derivative = error - self.prev_error
        self.prev_error = error

        # PID-adjusted basal rate (baseline ~1.0 U/hr)
        basal_adjustment = (
            self.kp * error + self.ki * self.integral + self.kd * derivative
        )
        basal_rate = max(0.0, min(5.0, 1.0 + basal_adjustment))

        # Simple correction bolus when glucose is very high
        if glucose > 200.0:
            bolus_dose = max(0.0, (glucose - 200.0) / 50.0)
        else:
            bolus_dose = 0.0

        # Safety: if glucose is dropping low, cut insulin aggressively
        if glucose < 90.0:
            basal_rate = max(0.0, basal_rate * 0.3)
            bolus_dose = 0.0
        if glucose < 70.0:
            basal_rate = 0.0
            bolus_dose = 0.0

        return GlucoAction(
            basal_rate=round(basal_rate, 4),
            bolus_dose=round(min(bolus_dose, 20.0), 4),
        )