iints-sdk-python35 0.1.15__tar.gz → 0.1.16__tar.gz

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 (106) hide show
  1. {iints_sdk_python35-0.1.15/src/iints_sdk_python35.egg-info → iints_sdk_python35-0.1.16}/PKG-INFO +1 -1
  2. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/pyproject.toml +1 -1
  3. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/__init__.py +4 -1
  4. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/api/base_algorithm.py +7 -0
  5. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/algorithms/correction_bolus.py +15 -6
  6. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/algorithms/fixed_basal_bolus.py +8 -2
  7. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/patient/models.py +41 -2
  8. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/safety/config.py +3 -0
  9. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/simulator.py +161 -10
  10. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/supervisor.py +38 -2
  11. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/validation/__init__.py +4 -0
  12. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/validation/schemas.py +11 -1
  13. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16/src/iints_sdk_python35.egg-info}/PKG-INFO +1 -1
  14. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/LICENSE +0 -0
  15. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/README.md +0 -0
  16. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/setup.cfg +0 -0
  17. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/__init__.py +0 -0
  18. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/algorithm_xray.py +0 -0
  19. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/baseline.py +0 -0
  20. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/clinical_benchmark.py +0 -0
  21. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/clinical_metrics.py +0 -0
  22. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/clinical_tir_analyzer.py +0 -0
  23. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/diabetes_metrics.py +0 -0
  24. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/edge_performance_monitor.py +0 -0
  25. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/explainability.py +0 -0
  26. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/explainable_ai.py +0 -0
  27. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/hardware_benchmark.py +0 -0
  28. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/metrics.py +0 -0
  29. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/reporting.py +0 -0
  30. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/sensor_filtering.py +0 -0
  31. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/analysis/validator.py +0 -0
  32. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/api/__init__.py +0 -0
  33. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/api/template_algorithm.py +0 -0
  34. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/assets/iints_logo.png +0 -0
  35. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/cli/__init__.py +0 -0
  36. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/cli/cli.py +0 -0
  37. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/__init__.py +0 -0
  38. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/algorithms/__init__.py +0 -0
  39. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/algorithms/battle_runner.py +0 -0
  40. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/algorithms/discovery.py +0 -0
  41. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/algorithms/hybrid_algorithm.py +0 -0
  42. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/algorithms/lstm_algorithm.py +0 -0
  43. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/algorithms/mock_algorithms.py +0 -0
  44. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/algorithms/pid_controller.py +0 -0
  45. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/algorithms/standard_pump_algo.py +0 -0
  46. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/device.py +0 -0
  47. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/device_manager.py +0 -0
  48. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/devices/__init__.py +0 -0
  49. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/devices/models.py +0 -0
  50. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/patient/__init__.py +0 -0
  51. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/patient/patient_factory.py +0 -0
  52. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/patient/profile.py +0 -0
  53. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/safety/__init__.py +0 -0
  54. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/safety/input_validator.py +0 -0
  55. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/safety/supervisor.py +0 -0
  56. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/simulation/__init__.py +0 -0
  57. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/core/simulation/scenario_parser.py +0 -0
  58. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/__init__.py +0 -0
  59. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/adapter.py +0 -0
  60. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/column_mapper.py +0 -0
  61. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/datasets.json +0 -0
  62. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/demo/__init__.py +0 -0
  63. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/demo/demo_cgm.csv +0 -0
  64. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/importer.py +0 -0
  65. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/ingestor.py +0 -0
  66. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/nightscout.py +0 -0
  67. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/quality_checker.py +0 -0
  68. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/registry.py +0 -0
  69. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/tidepool.py +0 -0
  70. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/universal_parser.py +0 -0
  71. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/virtual_patients/clinic_safe_baseline.yaml +0 -0
  72. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/virtual_patients/clinic_safe_hyper_challenge.yaml +0 -0
  73. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/virtual_patients/clinic_safe_hypo_prone.yaml +0 -0
  74. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/virtual_patients/clinic_safe_midnight.yaml +0 -0
  75. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/virtual_patients/clinic_safe_pizza.yaml +0 -0
  76. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/virtual_patients/clinic_safe_stress_meal.yaml +0 -0
  77. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/virtual_patients/default_patient.yaml +0 -0
  78. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/data/virtual_patients/patient_559_config.yaml +0 -0
  79. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/emulation/__init__.py +0 -0
  80. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/emulation/legacy_base.py +0 -0
  81. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/emulation/medtronic_780g.py +0 -0
  82. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/emulation/omnipod_5.py +0 -0
  83. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/emulation/tandem_controliq.py +0 -0
  84. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/highlevel.py +0 -0
  85. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/learning/__init__.py +0 -0
  86. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/learning/autonomous_optimizer.py +0 -0
  87. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/learning/learning_system.py +0 -0
  88. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/metrics.py +0 -0
  89. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/presets/__init__.py +0 -0
  90. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/presets/presets.json +0 -0
  91. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/scenarios/__init__.py +0 -0
  92. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/scenarios/generator.py +0 -0
  93. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/templates/__init__.py +0 -0
  94. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/templates/default_algorithm.py +0 -0
  95. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/templates/scenarios/__init__.py +0 -0
  96. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/templates/scenarios/example_scenario.json +0 -0
  97. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/utils/__init__.py +0 -0
  98. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/utils/plotting.py +0 -0
  99. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/visualization/__init__.py +0 -0
  100. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/visualization/cockpit.py +0 -0
  101. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints/visualization/uncertainty_cloud.py +0 -0
  102. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints_sdk_python35.egg-info/SOURCES.txt +0 -0
  103. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints_sdk_python35.egg-info/dependency_links.txt +0 -0
  104. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints_sdk_python35.egg-info/entry_points.txt +0 -0
  105. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints_sdk_python35.egg-info/requires.txt +0 -0
  106. {iints_sdk_python35-0.1.15 → iints_sdk_python35-0.1.16}/src/iints_sdk_python35.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iints-sdk-python35
3
- Version: 0.1.15
3
+ Version: 0.1.16
4
4
  Summary: A pre-clinical Edge-AI SDK for diabetes management validation.
5
5
  Author-email: Rune Bobbaers <rune.bobbaers@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/python35/IINTS-SDK
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "iints-sdk-python35"
7
- version = "0.1.15"
7
+ version = "0.1.16"
8
8
  authors = [
9
9
  { name="Rune Bobbaers", email="rune.bobbaers@gmail.com" },
10
10
  ]
@@ -3,7 +3,10 @@
3
3
  import pandas as pd # Required for type hints like pd.DataFrame
4
4
  from typing import Optional
5
5
 
6
- __version__ = "0.1.15"
6
+ __version__ = "0.1.16"
7
+
8
+ # Note to developers: this SDK is currently maintained by a single author.
9
+ # Please report bugs via GitHub issues and feel free to contribute fixes via PRs.
7
10
 
8
11
  # API Components for Algorithm Development
9
12
  from .api.base_algorithm import (
@@ -12,6 +12,13 @@ class AlgorithmInput:
12
12
  carb_intake: float = 0.0
13
13
  patient_state: Dict[str, Any] = field(default_factory=dict)
14
14
  current_time: float = 0.0 # Added current_time
15
+ carbs_on_board: float = 0.0
16
+ isf: Optional[float] = None
17
+ icr: Optional[float] = None
18
+ dia_minutes: Optional[float] = None
19
+ basal_rate_u_per_hr: Optional[float] = None
20
+ glucose_trend_mgdl_min: Optional[float] = None
21
+ predicted_glucose_30min: Optional[float] = None
15
22
 
16
23
 
17
24
  @dataclass
@@ -28,15 +28,21 @@ class CorrectionBolus(InsulinAlgorithm):
28
28
  def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
29
29
  self.why_log = [] # Clear the log for this prediction cycle
30
30
 
31
- basal_rate_units_per_minute = self.settings["fixed_basal_rate"] / 60.0
31
+ basal_rate = self.settings["fixed_basal_rate"]
32
+ if data.basal_rate_u_per_hr is not None:
33
+ basal_rate = float(data.basal_rate_u_per_hr)
34
+ basal_rate_units_per_minute = basal_rate / 60.0
32
35
  basal_insulin = basal_rate_units_per_minute * data.time_step
33
- self._log_reason(f"Basal insulin calculated ({self.settings['fixed_basal_rate']} U/hr)", "basal_delivery", basal_insulin)
36
+ self._log_reason(f"Basal insulin calculated ({basal_rate} U/hr)", "basal_delivery", basal_insulin)
34
37
 
35
38
  carb_intake = data.carb_intake
36
39
  meal_bolus = 0.0
37
40
  if carb_intake > 0:
38
- meal_bolus = carb_intake / self.settings["carb_ratio"]
39
- self._log_reason(f"Meal bolus calculated for {carb_intake:.0f}g carbs (CR: {self.settings['carb_ratio']})", "meal_response", meal_bolus)
41
+ carb_ratio = self.settings["carb_ratio"]
42
+ if data.icr is not None:
43
+ carb_ratio = float(data.icr)
44
+ meal_bolus = carb_intake / carb_ratio
45
+ self._log_reason(f"Meal bolus calculated for {carb_intake:.0f}g carbs (CR: {carb_ratio})", "meal_response", meal_bolus)
40
46
  else:
41
47
  self._log_reason("No meal bolus needed (no carb intake)", "meal_response", 0.0)
42
48
 
@@ -44,8 +50,11 @@ class CorrectionBolus(InsulinAlgorithm):
44
50
  correction_bolus = 0.0
45
51
  if data.current_glucose > self.settings["target_glucose"]:
46
52
  glucose_deviation = data.current_glucose - self.settings["target_glucose"]
47
- correction_bolus = glucose_deviation / self.settings["insulin_sensitivity_factor"]
48
- self._log_reason(f"Correction bolus calculated for high glucose (Current: {data.current_glucose:.0f}mg/dL, Target: {self.settings['target_glucose']:.0f}mg/dL, ISF: {self.settings['insulin_sensitivity_factor']})", "glucose_correction", correction_bolus)
53
+ isf = self.settings["insulin_sensitivity_factor"]
54
+ if data.isf is not None:
55
+ isf = float(data.isf)
56
+ correction_bolus = glucose_deviation / isf
57
+ self._log_reason(f"Correction bolus calculated for high glucose (Current: {data.current_glucose:.0f}mg/dL, Target: {self.settings['target_glucose']:.0f}mg/dL, ISF: {isf})", "glucose_correction", correction_bolus)
49
58
  else:
50
59
  self._log_reason(f"No correction bolus needed (glucose at or below target: {self.settings['target_glucose']:.0f}mg/dL)", "glucose_correction", 0.0)
51
60
 
@@ -32,13 +32,19 @@ class FixedBasalBolus(InsulinAlgorithm):
32
32
  Returns:
33
33
  Dict[str, Any]: Contains 'basal_insulin' and 'bolus_insulin' for the time step.
34
34
  """
35
- basal_rate_units_per_minute = self.settings["fixed_basal_rate"] / 60.0
35
+ basal_rate = self.settings["fixed_basal_rate"]
36
+ if data.basal_rate_u_per_hr is not None:
37
+ basal_rate = float(data.basal_rate_u_per_hr)
38
+ basal_rate_units_per_minute = basal_rate / 60.0
36
39
  basal_insulin = basal_rate_units_per_minute * data.time_step
37
40
 
38
41
  carb_intake = data.carb_intake
39
42
  bolus_insulin = 0.0
40
43
  if carb_intake > 0:
41
- bolus_insulin = carb_intake / self.settings["carb_ratio"]
44
+ carb_ratio = self.settings["carb_ratio"]
45
+ if data.icr is not None:
46
+ carb_ratio = float(data.icr)
47
+ bolus_insulin = carb_intake / carb_ratio
42
48
 
43
49
  return {
44
50
  "basal_insulin": basal_insulin,
@@ -21,7 +21,8 @@ class CustomPatientModel:
21
21
  meal_mismatch_epsilon: float = 1.0, # Factor for meal mismatch
22
22
  dawn_phenomenon_strength: float = 0.0, # mg/dL per hour
23
23
  dawn_start_hour: float = 4.0,
24
- dawn_end_hour: float = 8.0):
24
+ dawn_end_hour: float = 8.0,
25
+ carb_absorption_duration_minutes: float = 240.0):
25
26
  """
26
27
  Initializes the patient model with simplified parameters.
27
28
 
@@ -48,6 +49,7 @@ class CustomPatientModel:
48
49
  self.dawn_phenomenon_strength = dawn_phenomenon_strength
49
50
  self.dawn_start_hour = dawn_start_hour
50
51
  self.dawn_end_hour = dawn_end_hour
52
+ self.carb_absorption_duration_minutes = carb_absorption_duration_minutes
51
53
 
52
54
 
53
55
  self.initial_glucose = initial_glucose
@@ -156,12 +158,21 @@ class CustomPatientModel:
156
158
  # Simple model: carbs absorb over time, peaking around meal_effect_delay
157
159
  # This is a very rough approximation
158
160
  absorption_factor = 0.0
159
- if carb_event['time_since_intake'] <= 240: # Carbs absorb for ~4 hours
161
+ if carb_event['time_since_intake'] <= self.carb_absorption_duration_minutes: # Carbs absorb for ~4 hours
160
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)))
161
163
  carb_effect += carb_event['amount'] * absorption_factor
162
164
  new_active_carb_intakes.append(carb_event)
163
165
  # Carbs are "gone" after a while, or their effect is negligible
164
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
165
176
 
166
177
 
167
178
  # --- Exercise Effect ---
@@ -217,8 +228,36 @@ class CustomPatientModel:
217
228
  "current_glucose": self.current_glucose,
218
229
  "insulin_on_board": self.insulin_on_board,
219
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,
220
235
  }
221
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
+
222
261
  def get_state(self) -> Dict[str, Any]:
223
262
  return {
224
263
  "current_glucose": self.current_glucose,
@@ -23,6 +23,9 @@ class SafetyConfig:
23
23
  max_iob: float = 4.0
24
24
  trend_stop: float = -2.0
25
25
  hypo_cutoff: float = 70.0
26
+ max_basal_multiplier: float = 3.0
27
+ predicted_hypoglycemia_threshold: float = 60.0
28
+ predicted_hypoglycemia_horizon_minutes: int = 30
26
29
 
27
30
  # Simulation termination limits
28
31
  critical_glucose_threshold: float = 40.0
@@ -25,7 +25,19 @@ class SimulationLimitError(RuntimeError):
25
25
 
26
26
  class StressEvent:
27
27
  """Represents a discrete event that can occur during a simulation for stress testing."""
28
- def __init__(self, start_time: int, event_type: str, value: Any = None, reported_value: Any = None, absorption_delay_minutes: int = 0, duration: int = 0) -> None:
28
+ def __init__(
29
+ self,
30
+ start_time: int,
31
+ event_type: str,
32
+ value: Any = None,
33
+ reported_value: Any = None,
34
+ absorption_delay_minutes: int = 0,
35
+ duration: int = 0,
36
+ isf: Optional[float] = None,
37
+ icr: Optional[float] = None,
38
+ basal_rate: Optional[float] = None,
39
+ dia_minutes: Optional[float] = None,
40
+ ) -> None:
29
41
  """
30
42
  Args:
31
43
  start_time (int): The simulation time (in minutes) when the event should occur.
@@ -41,12 +53,29 @@ class StressEvent:
41
53
  self.reported_value = reported_value # New attribute
42
54
  self.absorption_delay_minutes = absorption_delay_minutes # New attribute
43
55
  self.duration = duration
56
+ self.isf = isf
57
+ self.icr = icr
58
+ self.basal_rate = basal_rate
59
+ self.dia_minutes = dia_minutes
44
60
 
45
61
  def __str__(self) -> str:
46
62
  reported_str = f", Reported: {self.reported_value}" if self.reported_value is not None else ""
63
+ ratio_str = ""
64
+ if self.event_type == "ratio_change":
65
+ parts = []
66
+ if self.isf is not None:
67
+ parts.append(f"ISF={self.isf}")
68
+ if self.icr is not None:
69
+ parts.append(f"ICR={self.icr}")
70
+ if self.basal_rate is not None:
71
+ parts.append(f"Basal={self.basal_rate}")
72
+ if self.dia_minutes is not None:
73
+ parts.append(f"DIA={self.dia_minutes}")
74
+ if parts:
75
+ ratio_str = ", " + ", ".join(parts)
47
76
  delay_str = f", Delay: {self.absorption_delay_minutes}m" if self.absorption_delay_minutes > 0 else ""
48
77
  duration_str = f", Duration: {self.duration}m" if self.duration > 0 else ""
49
- return f"Event(Time: {self.start_time}m, Type: {self.event_type}, Value: {self.value}{reported_str}{delay_str}{duration_str})"
78
+ return f"Event(Time: {self.start_time}m, Type: {self.event_type}, Value: {self.value}{reported_str}{ratio_str}{delay_str}{duration_str})"
50
79
 
51
80
  class Simulator:
52
81
  """
@@ -87,25 +116,27 @@ class Simulator:
87
116
  if self.seed is not None:
88
117
  np.random.seed(self.seed) # Set numpy seed for reproducibility
89
118
  # Potentially set other seeds here if other random modules are used (e.g., random.seed(self.seed))
90
- if safety_config is None:
91
- safety_config = SafetyConfig()
92
- self.supervisor = IndependentSupervisor(safety_config=safety_config)
93
- self.input_validator = InputValidator(safety_config=safety_config)
119
+ self.safety_config = safety_config or SafetyConfig()
120
+ self.supervisor = IndependentSupervisor(safety_config=self.safety_config)
121
+ self.input_validator = InputValidator(safety_config=self.safety_config)
94
122
  self.sensor_model = sensor_model or SensorModel(seed=seed)
95
123
  self.pump_model = pump_model or PumpModel(seed=seed)
96
124
  self.on_step = on_step
97
125
  self.meal_queue: List[Dict[str, Any]] = [] # Initialize meal queue for delayed absorption
98
126
  self.audit_log_path = audit_log_path
99
127
  self.enable_profiling = enable_profiling
100
- if safety_config is not None:
101
- critical_glucose_threshold = safety_config.critical_glucose_threshold
102
- critical_glucose_duration_minutes = safety_config.critical_glucose_duration_minutes
128
+ if self.safety_config is not None:
129
+ critical_glucose_threshold = self.safety_config.critical_glucose_threshold
130
+ critical_glucose_duration_minutes = self.safety_config.critical_glucose_duration_minutes
103
131
  self.critical_glucose_threshold = critical_glucose_threshold
104
132
  self.critical_glucose_duration_minutes = critical_glucose_duration_minutes
105
133
  self._critical_low_minutes = 0
106
134
  self._current_time = 0
107
135
  self._resume_state = False
108
136
  self._termination_info: Optional[Dict[str, Any]] = None
137
+ self._ratio_overrides: List[Dict[str, Any]] = []
138
+ self._base_ratio_state: Optional[Dict[str, float]] = None
139
+ self._previous_glucose_for_trend: Optional[float] = None
109
140
  self._profiling_samples: Dict[str, List[float]] = {
110
141
  "algorithm_latency_ms": [],
111
142
  "supervisor_latency_ms": [],
@@ -128,6 +159,67 @@ class Simulator:
128
159
  except IOError as e:
129
160
  logger.warning("Could not write to audit log file at %s. Error: %s", self.audit_log_path, e)
130
161
 
162
+ def _apply_ratio_overrides(self, current_time: float) -> Dict[str, float]:
163
+ if self._base_ratio_state is None:
164
+ self._base_ratio_state = self.patient_model.get_ratio_state()
165
+ effective = dict(self._base_ratio_state)
166
+ active = [
167
+ override
168
+ for override in self._ratio_overrides
169
+ if override["start_time"] <= current_time <= override["end_time"]
170
+ ]
171
+ if active:
172
+ latest = active[-1]
173
+ if latest.get("isf") is not None:
174
+ effective["isf"] = latest["isf"]
175
+ if latest.get("icr") is not None:
176
+ effective["icr"] = latest["icr"]
177
+ if latest.get("basal_rate_u_per_hr") is not None:
178
+ effective["basal_rate_u_per_hr"] = latest["basal_rate_u_per_hr"]
179
+ if latest.get("dia_minutes") is not None:
180
+ effective["dia_minutes"] = latest["dia_minutes"]
181
+
182
+ self.patient_model.set_ratio_state(
183
+ isf=effective.get("isf"),
184
+ icr=effective.get("icr"),
185
+ basal_rate=effective.get("basal_rate_u_per_hr"),
186
+ dia_minutes=effective.get("dia_minutes"),
187
+ )
188
+ try:
189
+ if effective.get("isf") is not None:
190
+ self.algorithm.set_isf(float(effective["isf"]))
191
+ if effective.get("icr") is not None:
192
+ self.algorithm.set_icr(float(effective["icr"]))
193
+ except Exception:
194
+ # Algorithm may ignore dynamic ratio updates; no hard failure.
195
+ pass
196
+ return effective
197
+
198
+ def _predict_glucose(
199
+ self,
200
+ current_glucose: float,
201
+ trend_mgdl_min: float,
202
+ iob_units: float,
203
+ cob_grams: float,
204
+ isf: float,
205
+ icr: float,
206
+ dia_minutes: float,
207
+ horizon_minutes: int,
208
+ carb_absorption_minutes: float,
209
+ ) -> float:
210
+ trend_component = trend_mgdl_min * horizon_minutes
211
+
212
+ insulin_component = 0.0
213
+ if dia_minutes > 0:
214
+ insulin_component = -iob_units * isf * min(horizon_minutes / dia_minutes, 1.0)
215
+
216
+ carb_component = 0.0
217
+ if icr > 0:
218
+ carb_effect_per_gram = isf / icr
219
+ carb_component = cob_grams * carb_effect_per_gram * min(horizon_minutes / carb_absorption_minutes, 1.0)
220
+
221
+ return current_glucose + trend_component + insulin_component + carb_component
222
+
131
223
  def add_stress_event(self, event: StressEvent) -> None:
132
224
  """Adds a stress event to be triggered during the simulation."""
133
225
  self.stress_events.append(event)
@@ -244,6 +336,9 @@ class Simulator:
244
336
  self._critical_low_minutes = 0
245
337
  self._current_time = 0
246
338
  self._termination_info = None
339
+ self._ratio_overrides = []
340
+ self._base_ratio_state = self.patient_model.get_ratio_state()
341
+ self._previous_glucose_for_trend = None
247
342
  else:
248
343
  self._resume_state = False
249
344
  if self.enable_profiling:
@@ -292,6 +387,18 @@ class Simulator:
292
387
  self.add_stress_event(end_event)
293
388
  elif event.event_type == 'exercise_end':
294
389
  self.patient_model.stop_exercise()
390
+ elif event.event_type == 'ratio_change':
391
+ duration = event.duration if event.duration > 0 else float("inf")
392
+ self._ratio_overrides.append(
393
+ {
394
+ "start_time": current_time,
395
+ "end_time": current_time + duration,
396
+ "isf": event.isf,
397
+ "icr": event.icr,
398
+ "basal_rate_u_per_hr": event.basal_rate,
399
+ "dia_minutes": event.dia_minutes,
400
+ }
401
+ )
295
402
 
296
403
  events_to_process_now.append(event) # Mark for removal from stress_events list
297
404
 
@@ -319,12 +426,44 @@ class Simulator:
319
426
  # This ensures even sensor error stress events are validated
320
427
  glucose_to_algorithm = self.input_validator.validate_glucose(glucose_to_algorithm, float(current_time))
321
428
 
429
+ # Apply dynamic ratio overrides (ISF/ICR/DIA/Basal) if any
430
+ ratio_state = self._apply_ratio_overrides(float(current_time))
431
+ effective_isf = float(ratio_state.get("isf", self.patient_model.insulin_sensitivity))
432
+ effective_icr = float(ratio_state.get("icr", self.patient_model.carb_factor))
433
+ effective_dia = float(ratio_state.get("dia_minutes", self.patient_model.insulin_action_duration))
434
+ effective_basal = float(ratio_state.get("basal_rate_u_per_hr", self.patient_model.basal_insulin_rate))
435
+
436
+ # Glucose trend (mg/dL per minute) based on sensor value
437
+ glucose_trend = 0.0
438
+ if self._previous_glucose_for_trend is not None:
439
+ glucose_trend = (glucose_to_algorithm - self._previous_glucose_for_trend) / float(self.time_step)
440
+ self._previous_glucose_for_trend = glucose_to_algorithm
441
+
442
+ predicted_glucose_30 = self._predict_glucose(
443
+ current_glucose=glucose_to_algorithm,
444
+ trend_mgdl_min=glucose_trend,
445
+ iob_units=self.patient_model.insulin_on_board,
446
+ cob_grams=self.patient_model.carbs_on_board,
447
+ isf=effective_isf,
448
+ icr=effective_icr,
449
+ dia_minutes=effective_dia,
450
+ horizon_minutes=self.safety_config.predicted_hypoglycemia_horizon_minutes,
451
+ carb_absorption_minutes=self.patient_model.carb_absorption_duration_minutes,
452
+ )
453
+
322
454
  # --- Algorithm Input ---
323
455
  algo_input = AlgorithmInput(
324
456
  current_glucose=glucose_to_algorithm,
325
457
  time_step=self.time_step,
326
458
  insulin_on_board=self.patient_model.insulin_on_board,
327
459
  carb_intake=algo_carb_intake_this_step, # Use algo's perspective of carbs
460
+ carbs_on_board=self.patient_model.carbs_on_board,
461
+ isf=effective_isf,
462
+ icr=effective_icr,
463
+ dia_minutes=effective_dia,
464
+ basal_rate_u_per_hr=effective_basal,
465
+ glucose_trend_mgdl_min=glucose_trend,
466
+ predicted_glucose_30min=predicted_glucose_30,
328
467
  patient_state=self.patient_model.get_patient_state(),
329
468
  current_time=float(current_time) # Pass current_time
330
469
  )
@@ -346,11 +485,17 @@ class Simulator:
346
485
 
347
486
  # --- Safety Supervision ---
348
487
  start_perf_time = time.perf_counter()
488
+ proposed_basal_units = float(insulin_output.get("basal_insulin", 0.0))
489
+ basal_limit_u_per_hr = effective_basal * self.safety_config.max_basal_multiplier
490
+ basal_limit_units = (basal_limit_u_per_hr / 60.0) * float(self.time_step)
349
491
  safety_result = self.supervisor.evaluate_safety(
350
492
  current_glucose=glucose_to_algorithm,
351
493
  proposed_insulin=algo_recommended_insulin,
352
494
  current_time=float(current_time),
353
- current_iob=self.patient_model.insulin_on_board
495
+ current_iob=self.patient_model.insulin_on_board,
496
+ predicted_glucose_30min=predicted_glucose_30,
497
+ basal_insulin_units=proposed_basal_units,
498
+ basal_limit_units=basal_limit_units,
354
499
  )
355
500
  supervisor_latency_ms = (time.perf_counter() - start_perf_time) * 1000
356
501
  if self.enable_profiling:
@@ -415,6 +560,8 @@ class Simulator:
415
560
  "time_minutes": current_time,
416
561
  "glucose_actual_mgdl": actual_glucose_reading,
417
562
  "glucose_to_algo_mgdl": glucose_to_algorithm,
563
+ "glucose_trend_mgdl_min": glucose_trend,
564
+ "predicted_glucose_30min": predicted_glucose_30,
418
565
  "delivered_insulin_units": delivered_insulin,
419
566
  "algo_recommended_insulin_units": algo_recommended_insulin,
420
567
  "sensor_status": sensor_reading.status,
@@ -426,6 +573,10 @@ class Simulator:
426
573
  "carb_intake_grams": patient_carb_intake_this_step,
427
574
  "patient_iob_units": self.patient_model.insulin_on_board,
428
575
  "patient_cob_grams": self.patient_model.carbs_on_board,
576
+ "effective_isf": effective_isf,
577
+ "effective_icr": effective_icr,
578
+ "effective_basal_rate_u_per_hr": effective_basal,
579
+ "effective_dia_minutes": effective_dia,
429
580
  "uncertainty": insulin_output.get("uncertainty", 0.0),
430
581
  "fallback_triggered": insulin_output.get("fallback_triggered", False),
431
582
  "safety_level": safety_level.value,
@@ -45,6 +45,8 @@ class IndependentSupervisor:
45
45
  max_iob=4.0, # Units
46
46
  trend_stop=-2.0, # mg/dL per minute
47
47
  hypo_cutoff=70.0, # mg/dL
48
+ predicted_hypoglycemia_threshold=60.0, # mg/dL
49
+ predicted_hypoglycemia_horizon_minutes=30, # minutes
48
50
  safety_config: Optional["SafetyConfig"] = None):
49
51
 
50
52
  if safety_config is not None:
@@ -57,6 +59,8 @@ class IndependentSupervisor:
57
59
  max_iob = safety_config.max_iob
58
60
  trend_stop = safety_config.trend_stop
59
61
  hypo_cutoff = safety_config.hypo_cutoff
62
+ predicted_hypoglycemia_threshold = safety_config.predicted_hypoglycemia_threshold
63
+ predicted_hypoglycemia_horizon_minutes = safety_config.predicted_hypoglycemia_horizon_minutes
60
64
 
61
65
  self.hypoglycemia_threshold = hypoglycemia_threshold
62
66
  self.severe_hypoglycemia_threshold = severe_hypoglycemia_threshold
@@ -67,6 +71,8 @@ class IndependentSupervisor:
67
71
  self.max_iob = max_iob
68
72
  self.trend_stop = trend_stop
69
73
  self.hypo_cutoff = hypo_cutoff
74
+ self.predicted_hypoglycemia_threshold = predicted_hypoglycemia_threshold
75
+ self.predicted_hypoglycemia_horizon_minutes = predicted_hypoglycemia_horizon_minutes
70
76
 
71
77
  # State tracking
72
78
  self.glucose_history: List[Tuple[float, float]] = []
@@ -75,8 +81,16 @@ class IndependentSupervisor:
75
81
  self.last_iob = 0.0
76
82
  self.dose_history: List[tuple] = []
77
83
 
78
- def evaluate_safety(self, current_glucose: float, proposed_insulin: float,
79
- current_time: float, current_iob: float = 0.0) -> Dict[str, Any]:
84
+ def evaluate_safety(
85
+ self,
86
+ current_glucose: float,
87
+ proposed_insulin: float,
88
+ current_time: float,
89
+ current_iob: float = 0.0,
90
+ predicted_glucose_30min: Optional[float] = None,
91
+ basal_insulin_units: Optional[float] = None,
92
+ basal_limit_units: Optional[float] = None,
93
+ ) -> Dict[str, Any]:
80
94
  """
81
95
  Evaluate safety of proposed insulin dose based on current glucose and IOB.
82
96
  Returns modified insulin dose and safety status.
@@ -90,6 +104,28 @@ class IndependentSupervisor:
90
104
  self.glucose_history.append((current_time, current_glucose))
91
105
  if len(self.glucose_history) > 20: # Keep last 20 readings
92
106
  self.glucose_history.pop(0)
107
+
108
+ # Predictive hypo guard (30-min horizon)
109
+ if predicted_glucose_30min is not None:
110
+ if predicted_glucose_30min <= self.predicted_hypoglycemia_threshold:
111
+ safety_status = SafetyLevel.EMERGENCY
112
+ proposed_insulin = 0
113
+ actions_taken.append(
114
+ f"PREDICTED_HYPO: {predicted_glucose_30min:.1f} mg/dL in "
115
+ f"{self.predicted_hypoglycemia_horizon_minutes} min"
116
+ )
117
+ self.emergency_mode = True
118
+
119
+ # Basal rate limit (relative to patient basal)
120
+ if basal_insulin_units is not None and basal_limit_units is not None:
121
+ if basal_insulin_units > basal_limit_units:
122
+ excess = basal_insulin_units - basal_limit_units
123
+ proposed_insulin = max(0.0, proposed_insulin - excess)
124
+ actions_taken.append(
125
+ f"BASAL_LIMIT: basal {basal_insulin_units:.2f}U exceeds "
126
+ f"limit {basal_limit_units:.2f}U"
127
+ )
128
+ safety_status = max(safety_status, SafetyLevel.WARNING, key=lambda x: x.value)
93
129
 
94
130
  # 1. Hard Hypo Cutoff (absolute stop)
95
131
  if current_glucose <= self.hypo_cutoff:
@@ -77,6 +77,10 @@ def build_stress_events(payloads: List[Dict[str, Any]]) -> List[StressEvent]:
77
77
  reported_value=event_data.get("reported_value"),
78
78
  absorption_delay_minutes=event_data.get("absorption_delay_minutes", 0),
79
79
  duration=event_data.get("duration", 0),
80
+ isf=event_data.get("isf"),
81
+ icr=event_data.get("icr"),
82
+ basal_rate=event_data.get("basal_rate"),
83
+ dia_minutes=event_data.get("dia_minutes"),
80
84
  )
81
85
  )
82
86
  return events
@@ -9,11 +9,15 @@ class StressEventModel(BaseModel):
9
9
  model_config = ConfigDict(extra="forbid")
10
10
 
11
11
  start_time: int = Field(ge=0)
12
- event_type: Literal["meal", "missed_meal", "sensor_error", "exercise", "exercise_end"]
12
+ event_type: Literal["meal", "missed_meal", "sensor_error", "exercise", "exercise_end", "ratio_change"]
13
13
  value: Optional[float] = None
14
14
  reported_value: Optional[float] = None
15
15
  absorption_delay_minutes: int = Field(default=0, ge=0)
16
16
  duration: int = Field(default=0, ge=0)
17
+ isf: Optional[float] = Field(default=None, gt=0)
18
+ icr: Optional[float] = Field(default=None, gt=0)
19
+ basal_rate: Optional[float] = Field(default=None, ge=0)
20
+ dia_minutes: Optional[float] = Field(default=None, gt=0)
17
21
 
18
22
  @model_validator(mode="after")
19
23
  def _check_required_fields(self) -> "StressEventModel":
@@ -26,6 +30,12 @@ class StressEventModel(BaseModel):
26
30
  if self.event_type == "sensor_error":
27
31
  if self.value is None:
28
32
  raise ValueError("sensor_error requires a value")
33
+ if self.event_type == "ratio_change":
34
+ if all(
35
+ val is None
36
+ for val in (self.isf, self.icr, self.basal_rate, self.dia_minutes)
37
+ ):
38
+ raise ValueError("ratio_change requires at least one ratio value (isf/icr/basal_rate/dia_minutes)")
29
39
  return self
30
40
 
31
41
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iints-sdk-python35
3
- Version: 0.1.15
3
+ Version: 0.1.16
4
4
  Summary: A pre-clinical Edge-AI SDK for diabetes management validation.
5
5
  Author-email: Rune Bobbaers <rune.bobbaers@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/python35/IINTS-SDK