iints-sdk-python35 0.0.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. iints/__init__.py +183 -0
  2. iints/analysis/__init__.py +12 -0
  3. iints/analysis/algorithm_xray.py +387 -0
  4. iints/analysis/baseline.py +92 -0
  5. iints/analysis/clinical_benchmark.py +198 -0
  6. iints/analysis/clinical_metrics.py +551 -0
  7. iints/analysis/clinical_tir_analyzer.py +136 -0
  8. iints/analysis/diabetes_metrics.py +43 -0
  9. iints/analysis/edge_efficiency.py +33 -0
  10. iints/analysis/edge_performance_monitor.py +315 -0
  11. iints/analysis/explainability.py +94 -0
  12. iints/analysis/explainable_ai.py +232 -0
  13. iints/analysis/hardware_benchmark.py +221 -0
  14. iints/analysis/metrics.py +117 -0
  15. iints/analysis/population_report.py +188 -0
  16. iints/analysis/reporting.py +345 -0
  17. iints/analysis/safety_index.py +311 -0
  18. iints/analysis/sensor_filtering.py +54 -0
  19. iints/analysis/validator.py +273 -0
  20. iints/api/__init__.py +0 -0
  21. iints/api/base_algorithm.py +307 -0
  22. iints/api/registry.py +103 -0
  23. iints/api/template_algorithm.py +195 -0
  24. iints/assets/iints_logo.png +0 -0
  25. iints/cli/__init__.py +0 -0
  26. iints/cli/cli.py +2598 -0
  27. iints/core/__init__.py +1 -0
  28. iints/core/algorithms/__init__.py +0 -0
  29. iints/core/algorithms/battle_runner.py +138 -0
  30. iints/core/algorithms/correction_bolus.py +95 -0
  31. iints/core/algorithms/discovery.py +92 -0
  32. iints/core/algorithms/fixed_basal_bolus.py +58 -0
  33. iints/core/algorithms/hybrid_algorithm.py +92 -0
  34. iints/core/algorithms/lstm_algorithm.py +138 -0
  35. iints/core/algorithms/mock_algorithms.py +162 -0
  36. iints/core/algorithms/pid_controller.py +88 -0
  37. iints/core/algorithms/standard_pump_algo.py +64 -0
  38. iints/core/device.py +0 -0
  39. iints/core/device_manager.py +64 -0
  40. iints/core/devices/__init__.py +3 -0
  41. iints/core/devices/models.py +160 -0
  42. iints/core/patient/__init__.py +9 -0
  43. iints/core/patient/bergman_model.py +341 -0
  44. iints/core/patient/models.py +285 -0
  45. iints/core/patient/patient_factory.py +117 -0
  46. iints/core/patient/profile.py +41 -0
  47. iints/core/safety/__init__.py +12 -0
  48. iints/core/safety/config.py +37 -0
  49. iints/core/safety/input_validator.py +95 -0
  50. iints/core/safety/supervisor.py +39 -0
  51. iints/core/simulation/__init__.py +0 -0
  52. iints/core/simulation/scenario_parser.py +61 -0
  53. iints/core/simulator.py +874 -0
  54. iints/core/supervisor.py +367 -0
  55. iints/data/__init__.py +53 -0
  56. iints/data/adapter.py +142 -0
  57. iints/data/column_mapper.py +398 -0
  58. iints/data/datasets.json +132 -0
  59. iints/data/demo/__init__.py +1 -0
  60. iints/data/demo/demo_cgm.csv +289 -0
  61. iints/data/importer.py +275 -0
  62. iints/data/ingestor.py +162 -0
  63. iints/data/nightscout.py +128 -0
  64. iints/data/quality_checker.py +550 -0
  65. iints/data/registry.py +166 -0
  66. iints/data/tidepool.py +38 -0
  67. iints/data/universal_parser.py +813 -0
  68. iints/data/virtual_patients/clinic_safe_baseline.yaml +9 -0
  69. iints/data/virtual_patients/clinic_safe_hyper_challenge.yaml +9 -0
  70. iints/data/virtual_patients/clinic_safe_hypo_prone.yaml +9 -0
  71. iints/data/virtual_patients/clinic_safe_midnight.yaml +9 -0
  72. iints/data/virtual_patients/clinic_safe_pizza.yaml +9 -0
  73. iints/data/virtual_patients/clinic_safe_stress_meal.yaml +9 -0
  74. iints/data/virtual_patients/default_patient.yaml +11 -0
  75. iints/data/virtual_patients/patient_559_config.yaml +11 -0
  76. iints/emulation/__init__.py +80 -0
  77. iints/emulation/legacy_base.py +414 -0
  78. iints/emulation/medtronic_780g.py +337 -0
  79. iints/emulation/omnipod_5.py +367 -0
  80. iints/emulation/tandem_controliq.py +393 -0
  81. iints/highlevel.py +451 -0
  82. iints/learning/__init__.py +3 -0
  83. iints/learning/autonomous_optimizer.py +194 -0
  84. iints/learning/learning_system.py +122 -0
  85. iints/metrics.py +34 -0
  86. iints/population/__init__.py +11 -0
  87. iints/population/generator.py +131 -0
  88. iints/population/runner.py +327 -0
  89. iints/presets/__init__.py +28 -0
  90. iints/presets/presets.json +114 -0
  91. iints/research/__init__.py +30 -0
  92. iints/research/config.py +68 -0
  93. iints/research/dataset.py +319 -0
  94. iints/research/losses.py +73 -0
  95. iints/research/predictor.py +329 -0
  96. iints/scenarios/__init__.py +3 -0
  97. iints/scenarios/generator.py +92 -0
  98. iints/templates/__init__.py +0 -0
  99. iints/templates/default_algorithm.py +91 -0
  100. iints/templates/scenarios/__init__.py +0 -0
  101. iints/templates/scenarios/chaos_insulin_stacking.json +29 -0
  102. iints/templates/scenarios/chaos_runaway_ai.json +25 -0
  103. iints/templates/scenarios/example_scenario.json +35 -0
  104. iints/templates/scenarios/exercise_stress.json +30 -0
  105. iints/utils/__init__.py +3 -0
  106. iints/utils/plotting.py +50 -0
  107. iints/utils/run_io.py +152 -0
  108. iints/validation/__init__.py +133 -0
  109. iints/validation/schemas.py +94 -0
  110. iints/visualization/__init__.py +34 -0
  111. iints/visualization/cockpit.py +691 -0
  112. iints/visualization/uncertainty_cloud.py +612 -0
  113. iints_sdk_python35-0.0.18.dist-info/METADATA +225 -0
  114. iints_sdk_python35-0.0.18.dist-info/RECORD +118 -0
  115. iints_sdk_python35-0.0.18.dist-info/WHEEL +5 -0
  116. iints_sdk_python35-0.0.18.dist-info/entry_points.txt +10 -0
  117. iints_sdk_python35-0.0.18.dist-info/licenses/LICENSE +28 -0
  118. iints_sdk_python35-0.0.18.dist-info/top_level.txt +1 -0
@@ -0,0 +1,341 @@
1
+ """
2
+ Bergman Minimal Model — IINTS-AF
3
+ ==================================
4
+ ODE-based patient model inspired by the Bergman Minimal Model with an
5
+ additional gut absorption compartment for realistic carbohydrate dynamics.
6
+
7
+ This provides a more physiologically accurate glucose simulation than the
8
+ default ``CustomPatientModel``, at the cost of higher computational load
9
+ (uses ``scipy.integrate.solve_ivp``).
10
+
11
+ The model tracks four state variables:
12
+
13
+ * **G** — plasma glucose concentration (mg/dL)
14
+ * **X** — remote insulin action (1/min)
15
+ * **I** — plasma insulin concentration (mU/L)
16
+ * **Q_gut** — gut glucose mass (mg)
17
+
18
+ References
19
+ ----------
20
+ * Bergman, R. N. et al. (1979). Quantitative estimation of insulin
21
+ sensitivity. *Am J Physiol*, 236(6), E667–E677.
22
+ * Dalla Man, C. et al. (2007). Meal Simulation Model of the Glucose-
23
+ Insulin System. *IEEE Trans Biomed Eng*, 54(10), 1740–1749.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+ from typing import Any, Dict, List, Optional
29
+
30
+ import numpy as np
31
+ from scipy.integrate import solve_ivp
32
+
33
+
34
+ @dataclass
35
+ class BergmanParameters:
36
+ """Physiological parameters for the Bergman Minimal Model."""
37
+
38
+ # --- Glucose sub-system ---
39
+ p1: float = 0.028 # 1/min — insulin-independent glucose uptake
40
+ p2: float = 0.025 # 1/min — rate of remote insulin degradation
41
+ p3: float = 5.0e-6 # (mU/L)^-1 min^-2 — insulin action gain
42
+ Gb: float = 120.0 # mg/dL — basal glucose concentration
43
+ Vg: float = 1.569 # dL/kg — glucose distribution volume
44
+
45
+ # --- Insulin sub-system ---
46
+ n: float = 0.23 # 1/min — fractional insulin degradation
47
+ Ib: float = 7.0 # mU/L — basal plasma insulin
48
+ Vi: float = 0.05 # L/kg — insulin distribution volume
49
+ gamma: float = 0.004 # (mU/L)/(mg/dL)/min — endogenous secretion gain
50
+ h: float = 80.0 # mg/dL — secretion glucose threshold
51
+
52
+ # --- Gut absorption ---
53
+ tau_meal: float = 40.0 # min — gastric emptying time constant
54
+ k_abs: float = 0.05 # 1/min — intestinal absorption rate constant
55
+ f_bio: float = 0.90 # — — bioavailability (fraction absorbed)
56
+
57
+ # --- Patient physical ---
58
+ body_weight_kg: float = 70.0
59
+
60
+
61
+ class BergmanPatientModel:
62
+ """
63
+ ODE-based patient model providing the same interface as
64
+ ``CustomPatientModel`` for drop-in use with the IINTS Simulator.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ basal_insulin_rate: float = 0.8,
70
+ insulin_sensitivity: float = 50.0,
71
+ carb_factor: float = 10.0,
72
+ initial_glucose: float = 120.0,
73
+ glucose_decay_rate: float = 0.05,
74
+ glucose_absorption_rate: float = 0.03,
75
+ insulin_action_duration: float = 300.0,
76
+ insulin_peak_time: float = 75.0,
77
+ meal_mismatch_epsilon: float = 1.0,
78
+ dawn_phenomenon_strength: float = 0.0,
79
+ dawn_start_hour: float = 4.0,
80
+ dawn_end_hour: float = 8.0,
81
+ carb_absorption_duration_minutes: float = 240.0,
82
+ bergman_params: Optional[BergmanParameters] = None,
83
+ ) -> None:
84
+ # Store clinical knobs (for ratio queries and compatibility)
85
+ self.basal_insulin_rate = basal_insulin_rate
86
+ self.insulin_sensitivity = insulin_sensitivity
87
+ self.carb_factor = carb_factor
88
+ self.initial_glucose = initial_glucose
89
+ self.glucose_decay_rate = glucose_decay_rate
90
+ self.glucose_absorption_rate = glucose_absorption_rate
91
+ self.insulin_action_duration = insulin_action_duration
92
+ self.insulin_peak_time = insulin_peak_time
93
+ self.meal_mismatch_epsilon = meal_mismatch_epsilon
94
+ self.dawn_phenomenon_strength = dawn_phenomenon_strength
95
+ self.dawn_start_hour = dawn_start_hour
96
+ self.dawn_end_hour = dawn_end_hour
97
+ self.carb_absorption_duration_minutes = carb_absorption_duration_minutes
98
+
99
+ # Bergman ODE parameters
100
+ self.params = bergman_params if bergman_params else BergmanParameters(Gb=initial_glucose)
101
+
102
+ # Exercise book-keeping
103
+ self.is_exercising = False
104
+ self.exercise_intensity = 0.0
105
+ self.exercise_glucose_consumption_rate = 1.5 # mg/dL per min at max
106
+
107
+ # Dose/carb trackers for IOB/COB (same format as CustomPatientModel)
108
+ self.active_insulin_doses: List[Dict[str, float]] = []
109
+ self.active_carb_intakes: List[Dict[str, float]] = []
110
+
111
+ # Derived scalar state
112
+ self.current_glucose = initial_glucose
113
+ self.insulin_on_board = 0.0
114
+ self.carbs_on_board = 0.0
115
+ self.meal_effect_delay = 30 # kept for API compat
116
+
117
+ # ODE state vector: [G, X, I, Q_gut]
118
+ self._state = np.array([
119
+ initial_glucose, # G (mg/dL)
120
+ 0.0, # X (1/min)
121
+ self.params.Ib, # I (mU/L)
122
+ 0.0, # Q_gut (mg)
123
+ ], dtype=np.float64)
124
+
125
+ self.reset()
126
+
127
+ # ------------------------------------------------------------------
128
+ # Public interface (mirrors CustomPatientModel exactly)
129
+ # ------------------------------------------------------------------
130
+
131
+ def reset(self) -> None:
132
+ """Reset to initial conditions."""
133
+ self._state = np.array([
134
+ self.initial_glucose, 0.0, self.params.Ib, 0.0,
135
+ ], dtype=np.float64)
136
+ self.current_glucose = self.initial_glucose
137
+ self.insulin_on_board = 0.0
138
+ self.carbs_on_board = 0.0
139
+ self.active_insulin_doses = []
140
+ self.active_carb_intakes = []
141
+ self.is_exercising = False
142
+ self.exercise_intensity = 0.0
143
+
144
+ def start_exercise(self, intensity: float) -> None:
145
+ if not (0.0 <= intensity <= 1.0):
146
+ raise ValueError("Exercise intensity must be between 0.0 and 1.0")
147
+ self.is_exercising = True
148
+ self.exercise_intensity = intensity
149
+
150
+ def stop_exercise(self) -> None:
151
+ self.is_exercising = False
152
+ self.exercise_intensity = 0.0
153
+
154
+ def update(
155
+ self,
156
+ time_step: float,
157
+ delivered_insulin: float,
158
+ carb_intake: float = 0.0,
159
+ current_time: Optional[float] = None,
160
+ **kwargs,
161
+ ) -> float:
162
+ """Advance the model by *time_step* minutes and return new glucose."""
163
+ true_carbs = carb_intake * self.meal_mismatch_epsilon
164
+
165
+ # --- Track IOB (same bookkeeping as CustomPatientModel) ---
166
+ if delivered_insulin > 0.001:
167
+ self.active_insulin_doses.append({"amount": delivered_insulin, "age": 0.0})
168
+ for d in self.active_insulin_doses:
169
+ d["age"] += time_step
170
+ self.active_insulin_doses = [
171
+ d for d in self.active_insulin_doses
172
+ if d["age"] <= self.insulin_action_duration
173
+ ]
174
+ self.insulin_on_board = sum(
175
+ d["amount"] * max(0.0, (self.insulin_action_duration - d["age"]) / self.insulin_action_duration)
176
+ for d in self.active_insulin_doses
177
+ )
178
+
179
+ # --- Track COB ---
180
+ if true_carbs > 0:
181
+ self.active_carb_intakes.append({"amount": true_carbs, "time_since_intake": 0.0})
182
+ for c in self.active_carb_intakes:
183
+ c["time_since_intake"] += time_step
184
+ self.active_carb_intakes = [
185
+ c for c in self.active_carb_intakes
186
+ if c["time_since_intake"] <= self.carb_absorption_duration_minutes
187
+ ]
188
+ self.carbs_on_board = sum(
189
+ c["amount"] * max(0.0, 1.0 - c["time_since_intake"] / self.carb_absorption_duration_minutes)
190
+ for c in self.active_carb_intakes
191
+ )
192
+
193
+ # --- Inject carbs into gut compartment ---
194
+ # Add carbs as glucose mass (mg) into Q_gut
195
+ if true_carbs > 0:
196
+ self._state[3] += true_carbs * 1000.0 # g -> mg
197
+
198
+ # --- Prepare exogenous insulin rate ---
199
+ # Convert Units to mU, spread evenly over time_step (mU/min)
200
+ insulin_rate = (delivered_insulin * 1000.0) / max(time_step, 0.001)
201
+
202
+ # --- Solve ODE ---
203
+ ct = current_time if current_time is not None else 0.0
204
+ sol = solve_ivp(
205
+ fun=lambda t, y: self._ode(t, y, insulin_rate, ct),
206
+ t_span=(0.0, time_step),
207
+ y0=self._state,
208
+ method="RK45",
209
+ max_step=1.0,
210
+ rtol=1e-6,
211
+ atol=1e-8,
212
+ )
213
+
214
+ self._state = sol.y[:, -1].copy()
215
+ # Floor glucose at 20 mg/dL (physiological minimum)
216
+ self._state[0] = max(20.0, self._state[0])
217
+ # Clamp non-negative for other compartments
218
+ self._state[1] = max(0.0, self._state[1])
219
+ self._state[2] = max(0.0, self._state[2])
220
+ self._state[3] = max(0.0, self._state[3])
221
+
222
+ self.current_glucose = float(self._state[0])
223
+ return self.current_glucose
224
+
225
+ def get_current_glucose(self) -> float:
226
+ return self.current_glucose
227
+
228
+ def trigger_event(self, event_type: str, value: Any) -> None:
229
+ pass # handled by the simulator
230
+
231
+ def get_patient_state(self) -> Dict[str, float]:
232
+ return {
233
+ "current_glucose": self.current_glucose,
234
+ "insulin_on_board": self.insulin_on_board,
235
+ "carbs_on_board": self.carbs_on_board,
236
+ "basal_rate_u_per_hr": self.basal_insulin_rate,
237
+ "isf": self.insulin_sensitivity,
238
+ "icr": self.carb_factor,
239
+ "dia_minutes": self.insulin_action_duration,
240
+ "plasma_insulin_mU_L": float(self._state[2]),
241
+ "remote_insulin_action": float(self._state[1]),
242
+ "gut_glucose_mg": float(self._state[3]),
243
+ }
244
+
245
+ def get_ratio_state(self) -> Dict[str, float]:
246
+ return {
247
+ "basal_rate_u_per_hr": self.basal_insulin_rate,
248
+ "isf": self.insulin_sensitivity,
249
+ "icr": self.carb_factor,
250
+ "dia_minutes": self.insulin_action_duration,
251
+ }
252
+
253
+ def set_ratio_state(
254
+ self,
255
+ isf: Optional[float] = None,
256
+ icr: Optional[float] = None,
257
+ basal_rate: Optional[float] = None,
258
+ dia_minutes: Optional[float] = None,
259
+ ) -> None:
260
+ if isf is not None:
261
+ self.insulin_sensitivity = float(isf)
262
+ if icr is not None:
263
+ self.carb_factor = float(icr)
264
+ if basal_rate is not None:
265
+ self.basal_insulin_rate = float(basal_rate)
266
+ if dia_minutes is not None:
267
+ self.insulin_action_duration = float(dia_minutes)
268
+
269
+ def get_state(self) -> Dict[str, Any]:
270
+ return {
271
+ "ode_state": self._state.tolist(),
272
+ "current_glucose": self.current_glucose,
273
+ "insulin_on_board": self.insulin_on_board,
274
+ "carbs_on_board": self.carbs_on_board,
275
+ "active_insulin_doses": self.active_insulin_doses,
276
+ "active_carb_intakes": self.active_carb_intakes,
277
+ "is_exercising": self.is_exercising,
278
+ "exercise_intensity": self.exercise_intensity,
279
+ }
280
+
281
+ def set_state(self, state: Dict[str, Any]) -> None:
282
+ if "ode_state" in state:
283
+ self._state = np.array(state["ode_state"], dtype=np.float64)
284
+ self.current_glucose = state.get("current_glucose", self.current_glucose)
285
+ self.insulin_on_board = state.get("insulin_on_board", self.insulin_on_board)
286
+ self.carbs_on_board = state.get("carbs_on_board", self.carbs_on_board)
287
+ self.active_insulin_doses = state.get("active_insulin_doses", [])
288
+ self.active_carb_intakes = state.get("active_carb_intakes", [])
289
+ self.is_exercising = state.get("is_exercising", False)
290
+ self.exercise_intensity = state.get("exercise_intensity", 0.0)
291
+
292
+ # ------------------------------------------------------------------
293
+ # ODE right-hand-side
294
+ # ------------------------------------------------------------------
295
+
296
+ def _ode(
297
+ self,
298
+ t: float,
299
+ y: np.ndarray,
300
+ u_insulin_mu_per_min: float,
301
+ current_time: float,
302
+ ) -> np.ndarray:
303
+ G, X, I, Q_gut = y
304
+ p = self.params
305
+
306
+ Vg_abs = p.Vg * p.body_weight_kg # dL
307
+ Vi_abs = p.Vi * p.body_weight_kg # L
308
+
309
+ # --- Glucose rate of appearance from gut ---
310
+ Ra = (p.k_abs * Q_gut) / Vg_abs # mg/dL/min
311
+
312
+ # --- Dawn phenomenon ---
313
+ dawn = 0.0
314
+ if self.dawn_phenomenon_strength > 0:
315
+ minutes_in_day = current_time % 1440
316
+ ds = self.dawn_start_hour * 60
317
+ de = self.dawn_end_hour * 60
318
+ if ds <= minutes_in_day <= de:
319
+ dawn = self.dawn_phenomenon_strength / 60.0 # mg/dL/min
320
+
321
+ # --- Exercise ---
322
+ exercise = 0.0
323
+ if self.is_exercising:
324
+ exercise = self.exercise_intensity * self.exercise_glucose_consumption_rate
325
+
326
+ # --- dG/dt ---
327
+ dGdt = -(p.p1 + X) * G + p.p1 * p.Gb + Ra + dawn - exercise
328
+
329
+ # --- dX/dt ---
330
+ dXdt = -p.p2 * X + p.p3 * max(I - p.Ib, 0.0)
331
+
332
+ # --- dI/dt ---
333
+ # Endogenous pancreatic secretion (blunted in T1D, but kept for generality)
334
+ secretion = p.gamma * max(G - p.h, 0.0)
335
+ dIdt = -p.n * (I - p.Ib) + secretion + u_insulin_mu_per_min / Vi_abs
336
+
337
+ # --- dQ_gut/dt ---
338
+ # Q_gut decays as glucose is absorbed into plasma
339
+ dQ_gut_dt = -p.k_abs * Q_gut
340
+
341
+ return np.array([dGdt, dXdt, dIdt, dQ_gut_dt])
@@ -0,0 +1,285 @@
1
+ import numpy as np
2
+ from typing import Dict, Any, Optional
3
+
4
+ # Use custom patient model as default to avoid simglucose dependency issues
5
+ # from simglucose.simulation.env import T1DSimEnv
6
+ # from simglucose.patient.t1dpatient import T1DPatient
7
+ # from simglucose.sensor.cgm import CGMSensor
8
+ # from simglucose.actuator.pump import InsulinPump
9
+ # from simglucose.controller.base import Action
10
+
11
+ class CustomPatientModel:
12
+ """
13
+ A simplified patient model for simulating blood glucose dynamics.
14
+ This model is intended for educational and stress-testing purposes, not for clinical accuracy.
15
+ """
16
+ def __init__(self, basal_insulin_rate: float = 0.8, insulin_sensitivity: float = 50.0,
17
+ carb_factor: float = 10.0, glucose_decay_rate: float = 0.002,
18
+ initial_glucose: float = 120.0, glucose_absorption_rate: float = 0.03,
19
+ insulin_action_duration: float = 300.0, # minutes, e.g., 5 hours
20
+ insulin_peak_time: float = 75.0, # minutes
21
+ meal_mismatch_epsilon: float = 1.0, # Factor for meal mismatch
22
+ dawn_phenomenon_strength: float = 0.0, # mg/dL per hour
23
+ dawn_start_hour: float = 4.0,
24
+ dawn_end_hour: float = 8.0,
25
+ carb_absorption_duration_minutes: float = 240.0):
26
+ """
27
+ Initializes the patient model with simplified parameters.
28
+
29
+ Args:
30
+ basal_insulin_rate (float): Basal insulin rate in U/hr.
31
+ insulin_sensitivity (float): How much 1 unit of insulin lowers glucose (mg/dL per Unit).
32
+ carb_factor (float): How many carbs (g) are covered by 1 unit of insulin.
33
+ glucose_decay_rate (float): Rate at which glucose naturally decreases (e.g., due to metabolism).
34
+ initial_glucose (float): Starting blood glucose level (mg/dL).
35
+ glucose_absorption_rate (float): Rate at which carbs are absorbed into glucose.
36
+ insulin_action_duration (float): Duration of insulin action (DIA) in minutes.
37
+ insulin_peak_time (float): Time to peak insulin activity in minutes.
38
+ meal_mismatch_epsilon (float): The multiplier for carb intake to simulate meal size errors.
39
+ `true_carbs = announced_carbs * meal_mismatch_epsilon`. Defaults to 1.0.
40
+ """
41
+ self.basal_insulin_rate = basal_insulin_rate
42
+ self.insulin_sensitivity = insulin_sensitivity
43
+ self.carb_factor = carb_factor
44
+ self.glucose_decay_rate = glucose_decay_rate
45
+ self.glucose_absorption_rate = glucose_absorption_rate
46
+ self.insulin_action_duration = insulin_action_duration
47
+ self.insulin_peak_time = insulin_peak_time
48
+ self.meal_mismatch_epsilon = meal_mismatch_epsilon
49
+ self.dawn_phenomenon_strength = dawn_phenomenon_strength
50
+ self.dawn_start_hour = dawn_start_hour
51
+ self.dawn_end_hour = dawn_end_hour
52
+ self.carb_absorption_duration_minutes = carb_absorption_duration_minutes
53
+
54
+
55
+ self.initial_glucose = initial_glucose
56
+ self.current_glucose = initial_glucose
57
+ self.insulin_on_board = 0.0 # Units of insulin still active
58
+ self.carbs_on_board = 0.0 # Grams of carbs still being absorbed
59
+ self.meal_effect_delay = 30 # minutes for carb absorption to peak
60
+
61
+ # Exercise state
62
+ self.is_exercising = False
63
+ self.exercise_intensity = 0.0 # 0.0 to 1.0
64
+ self.exercise_glucose_consumption_rate = 1.5 # mg/dL per minute at max intensity
65
+
66
+ self.reset() # Call reset to ensure initial state consistency
67
+
68
+ def reset(self):
69
+ """Resets the patient's state to initial conditions."""
70
+ self.current_glucose = self.initial_glucose
71
+ self.insulin_on_board = 0.0
72
+ self.carbs_on_board = 0.0
73
+ self.active_insulin_doses = [] # List of {'amount': float, 'age': float}
74
+ self.active_carb_intakes = [] # (carb_amount, time_since_intake)
75
+ self.is_exercising = False
76
+ self.exercise_intensity = 0.0
77
+
78
+ def start_exercise(self, intensity: float):
79
+ """Starts an exercise session."""
80
+ if not (0.0 <= intensity <= 1.0):
81
+ raise ValueError("Exercise intensity must be between 0.0 and 1.0")
82
+ self.is_exercising = True
83
+ self.exercise_intensity = intensity
84
+ print(f"INFO: Patient started exercise with intensity {intensity:.2f}")
85
+
86
+ def stop_exercise(self):
87
+ """Stops an exercise session."""
88
+ self.is_exercising = False
89
+ self.exercise_intensity = 0.0
90
+ print("INFO: Patient stopped exercise.")
91
+
92
+ def update(self, time_step: float, delivered_insulin: float, carb_intake: float = 0.0, current_time: Optional[float] = None, **kwargs) -> float:
93
+ """
94
+ Updates the patient's glucose level over a given time step.
95
+
96
+ Args:
97
+ time_step (float): The duration of the simulation step in minutes.
98
+ delivered_insulin (float): Total insulin delivered in this time step (e.g., bolus + basal).
99
+ carb_intake (float): Carbohydrates consumed in this time step (grams).
100
+ **kwargs: Additional factors like exercise, stress (not yet implemented in detail).
101
+
102
+ Returns:
103
+ float: The new current blood glucose level.
104
+ """
105
+ # Convert time_step to hours for basal rate
106
+ time_step_hours = time_step / 60.0
107
+
108
+ # --- Insulin effect ---
109
+ # Add new insulin dose
110
+ if delivered_insulin > 0.001:
111
+ self.active_insulin_doses.append({'amount': delivered_insulin, 'age': 0.0})
112
+
113
+ # Update ages and remove old doses
114
+ for dose in self.active_insulin_doses:
115
+ dose['age'] += time_step
116
+
117
+ self.active_insulin_doses = [d for d in self.active_insulin_doses if d['age'] <= self.insulin_action_duration]
118
+
119
+ # Calculate IOB (Insulin on Board) using a linear decay model for remaining insulin
120
+ iob = 0.0
121
+ for dose in self.active_insulin_doses:
122
+ remaining_fraction = (self.insulin_action_duration - dose['age']) / self.insulin_action_duration
123
+ iob += dose['amount'] * remaining_fraction
124
+ self.insulin_on_board = iob
125
+
126
+ # Calculate Insulin Action for this time step using a bilinear activity curve
127
+ total_insulin_action = 0.0
128
+ for dose in self.active_insulin_doses:
129
+ if dose['age'] < self.insulin_peak_time:
130
+ # Action ramps up
131
+ action_factor = dose['age'] / self.insulin_peak_time
132
+ else:
133
+ # Action ramps down
134
+ action_factor = (self.insulin_action_duration - dose['age']) / (self.insulin_action_duration - self.insulin_peak_time)
135
+
136
+ # Normalize the action so the total effect of 1U of insulin equals 1U * insulin_sensitivity
137
+ # The area under the bilinear activity curve is 0.5 * peak_time + 0.5 * (duration - peak_time) = 0.5 * duration
138
+ # The action for this step is a fraction of the total dose effect.
139
+ dose_action_this_step = dose['amount'] * action_factor * (time_step / (0.5 * self.insulin_action_duration))
140
+ total_insulin_action += dose_action_this_step
141
+
142
+ insulin_effect = total_insulin_action * self.insulin_sensitivity
143
+
144
+ # --- Carb effect ---
145
+ # The 'carb_intake' parameter represents the "announced carbs" by the AI.
146
+ # The 'true_carbs' are the actual carbs the patient's body processes,
147
+ # simulated using the meal_mismatch_epsilon parameter.
148
+ true_carbs = carb_intake * self.meal_mismatch_epsilon
149
+ # Add new carbs
150
+ if true_carbs > 0:
151
+ self.active_carb_intakes.append({'amount': true_carbs, 'time_since_intake': 0.0})
152
+
153
+ # Process active carbs
154
+ carb_effect = 0.0
155
+ new_active_carb_intakes = []
156
+ for carb_event in self.active_carb_intakes:
157
+ carb_event['time_since_intake'] += time_step
158
+ # Simple model: carbs absorb over time, peaking around meal_effect_delay
159
+ # This is a very rough approximation
160
+ absorption_factor = 0.0
161
+ if carb_event['time_since_intake'] <= self.carb_absorption_duration_minutes: # Carbs absorb for ~4 hours
162
+ absorption_factor = self.glucose_absorption_rate * (np.exp(-carb_event['time_since_intake'] / self.meal_effect_delay) - np.exp(-carb_event['time_since_intake'] / (self.meal_effect_delay * 0.5)))
163
+ carb_effect += carb_event['amount'] * absorption_factor
164
+ new_active_carb_intakes.append(carb_event)
165
+ # Carbs are "gone" after a while, or their effect is negligible
166
+ self.active_carb_intakes = new_active_carb_intakes
167
+ # Estimate carbs on board based on remaining absorption window
168
+ carb_remaining = 0.0
169
+ for carb_event in self.active_carb_intakes:
170
+ remaining_fraction = max(
171
+ 0.0,
172
+ 1.0 - (carb_event['time_since_intake'] / self.carb_absorption_duration_minutes),
173
+ )
174
+ carb_remaining += carb_event['amount'] * remaining_fraction
175
+ self.carbs_on_board = carb_remaining
176
+
177
+
178
+ # --- Exercise Effect ---
179
+ exercise_effect = 0.0
180
+ if self.is_exercising:
181
+ exercise_effect = self.exercise_intensity * self.exercise_glucose_consumption_rate * time_step
182
+
183
+
184
+ # --- Basal metabolic glucose production/consumption (simplified) ---
185
+ basal_glucose_change = -self.glucose_decay_rate * self.current_glucose * time_step
186
+
187
+ # --- Dawn phenomenon effect ---
188
+ dawn_effect = 0.0
189
+ if current_time is not None and self.dawn_phenomenon_strength > 0:
190
+ minutes_in_day = current_time % 1440
191
+ dawn_start_min = self.dawn_start_hour * 60
192
+ dawn_end_min = self.dawn_end_hour * 60
193
+ if dawn_start_min <= minutes_in_day <= dawn_end_min:
194
+ dawn_effect = (self.dawn_phenomenon_strength / 60.0) * time_step
195
+
196
+ # --- Update glucose ---
197
+ delta_glucose = carb_effect - insulin_effect - exercise_effect + basal_glucose_change + dawn_effect
198
+ self.current_glucose = max(20, self.current_glucose + delta_glucose) # Prevent glucose from going too low (hypoglycemia)
199
+
200
+ return self.current_glucose
201
+
202
+ def get_current_glucose(self) -> float:
203
+ """Returns the current blood glucose level."""
204
+ return self.current_glucose
205
+
206
+ def trigger_event(self, event_type: str, value: Any):
207
+ """
208
+ Triggers a specific event for stress testing (e.g., missed meal, sensor error).
209
+
210
+ Args:
211
+ event_type (str): Type of event ('missed_meal', 'sensor_error', 'exercise', etc.).
212
+ value (Any): Value associated with the event (e.g., carb amount for missed meal).
213
+ """
214
+ if event_type == 'missed_meal':
215
+ print(f"STRESS EVENT: Missed meal of {value}g carbs!")
216
+ # This event primarily affects the *simulator's* input to the algorithm,
217
+ # but the patient model needs to know if carbs are actually consumed.
218
+ # For now, it's a print statement. Actual effect handled in simulator.
219
+ elif event_type == 'sensor_error':
220
+ print(f"STRESS EVENT: Sensor error - returning {value} as glucose reading.")
221
+ # This event will be handled by the simulator intercepting glucose readings.
222
+ else:
223
+ print(f"Unknown stress event: {event_type}")
224
+
225
+ # Helper function for visualization/logging
226
+ def get_patient_state(self) -> Dict[str, float]:
227
+ return {
228
+ "current_glucose": self.current_glucose,
229
+ "insulin_on_board": self.insulin_on_board,
230
+ "carbs_on_board": self.carbs_on_board,
231
+ "basal_rate_u_per_hr": self.basal_insulin_rate,
232
+ "isf": self.insulin_sensitivity,
233
+ "icr": self.carb_factor,
234
+ "dia_minutes": self.insulin_action_duration,
235
+ }
236
+
237
+ def get_ratio_state(self) -> Dict[str, float]:
238
+ return {
239
+ "basal_rate_u_per_hr": self.basal_insulin_rate,
240
+ "isf": self.insulin_sensitivity,
241
+ "icr": self.carb_factor,
242
+ "dia_minutes": self.insulin_action_duration,
243
+ }
244
+
245
+ def set_ratio_state(
246
+ self,
247
+ isf: Optional[float] = None,
248
+ icr: Optional[float] = None,
249
+ basal_rate: Optional[float] = None,
250
+ dia_minutes: Optional[float] = None,
251
+ ) -> None:
252
+ if isf is not None:
253
+ self.insulin_sensitivity = float(isf)
254
+ if icr is not None:
255
+ self.carb_factor = float(icr)
256
+ if basal_rate is not None:
257
+ self.basal_insulin_rate = float(basal_rate)
258
+ if dia_minutes is not None:
259
+ self.insulin_action_duration = float(dia_minutes)
260
+
261
+ def get_state(self) -> Dict[str, Any]:
262
+ return {
263
+ "current_glucose": self.current_glucose,
264
+ "insulin_on_board": self.insulin_on_board,
265
+ "carbs_on_board": self.carbs_on_board,
266
+ "active_insulin_doses": self.active_insulin_doses,
267
+ "active_carb_intakes": self.active_carb_intakes,
268
+ "is_exercising": self.is_exercising,
269
+ "exercise_intensity": self.exercise_intensity,
270
+ }
271
+
272
+ def set_state(self, state: Dict[str, Any]) -> None:
273
+ self.current_glucose = state.get("current_glucose", self.current_glucose)
274
+ self.insulin_on_board = state.get("insulin_on_board", self.insulin_on_board)
275
+ self.carbs_on_board = state.get("carbs_on_board", self.carbs_on_board)
276
+ self.active_insulin_doses = state.get("active_insulin_doses", [])
277
+ self.active_carb_intakes = state.get("active_carb_intakes", [])
278
+ self.is_exercising = state.get("is_exercising", False)
279
+ self.exercise_intensity = state.get("exercise_intensity", 0.0)
280
+
281
+ # Alias for easy import
282
+ PatientModel = CustomPatientModel
283
+
284
+ # SimglucosePatientModel commented out due to dependency issues
285
+ # Uncomment and install simglucose for FDA-approved virtual patients